mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 11:55:23 +00:00
Merge main into fix/schema-expected-phrases
This commit is contained in:
Generated
+1
@@ -6476,6 +6476,7 @@ dependencies = [
|
||||
"vectorcraft-plugins",
|
||||
"vectorcraft-text",
|
||||
"vectorcraft-three-d",
|
||||
"vectorcraft-trace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -150,7 +150,7 @@ fn planar(d: &mut tiff::decoder::Decoder<Cursor<&[u8]>>) -> bool {
|
||||
/// stores each pixel's samples together or one plane per channel. `None` when they don't decode to
|
||||
/// that many samples, which includes images whose planes together need more than the decoder's
|
||||
/// limit (256 MB of samples).
|
||||
fn read_samples(d: &mut tiff::decoder::Decoder<Cursor<&[u8]>>, n: usize, per: usize) -> Option<Vec<u8>> {
|
||||
pub(crate) fn read_samples(d: &mut tiff::decoder::Decoder<Cursor<&[u8]>>, n: usize, per: usize) -> Option<Vec<u8>> {
|
||||
let mut buf = tiff::decoder::DecodingResult::U8(Vec::new());
|
||||
let layout = d.read_image_to_buffer(&mut buf).ok()?;
|
||||
let data = match buf {
|
||||
|
||||
@@ -62,6 +62,7 @@ pub mod rastersettings;
|
||||
mod reach;
|
||||
pub mod recolor;
|
||||
pub mod selection;
|
||||
pub mod separated;
|
||||
pub mod setup;
|
||||
pub mod shaper;
|
||||
pub mod slices;
|
||||
|
||||
+24
-13
@@ -819,7 +819,8 @@ impl Node {
|
||||
impl Node {
|
||||
/// Whether this object shows any transparency, itself or anything inside it: object, fill or
|
||||
/// stroke opacity below 100%, a blend mode, an opacity mask, effects, transparent gradient
|
||||
/// stops or a colorized image. Hidden objects show none.
|
||||
/// stops (characters' fills and strokes included) or a colorized image. Hidden objects show
|
||||
/// none.
|
||||
pub fn shows_transparency(&self) -> bool {
|
||||
self.shows_transparency_with(&|_| true)
|
||||
}
|
||||
@@ -827,21 +828,31 @@ impl Node {
|
||||
/// [`Self::shows_transparency`], counting a colorized image by itself only when `colorize`
|
||||
/// accepts it, for files that write some colorized images in their colorize paint (EPS).
|
||||
pub fn shows_transparency_with(&self, colorize: &dyn Fn(&Node) -> bool) -> bool {
|
||||
self.visible && (self.own_transparency(colorize) || self.children().is_some_and(|ch| ch.iter().any(|c| c.shows_transparency_with(colorize))))
|
||||
}
|
||||
|
||||
/// [`Self::shows_transparency`] of the object alone, what it holds left out.
|
||||
pub fn shows_own_transparency(&self) -> bool {
|
||||
self.visible && self.own_transparency(&|_| true)
|
||||
}
|
||||
|
||||
/// The object's own transparency, as [`Self::shows_transparency_with`] counts it.
|
||||
fn own_transparency(&self, colorize: &dyn Fn(&Node) -> bool) -> bool {
|
||||
use vectorcraft_color::Paint;
|
||||
let see_through = |p: &Paint| matches!(p, Paint::Gradient(g) if g.gradient.stops.iter().any(|s| s.opacity < 1.0));
|
||||
let effects = |fx: &[crate::Effect]| fx.iter().any(|e| e.visible);
|
||||
self.visible
|
||||
&& (self.opacity < 1.0
|
||||
|| self.blend != BlendMode::Normal
|
||||
|| self.extras().mask.as_ref().is_some_and(|m| !m.disabled)
|
||||
|| (matches!(self.kind, NodeKind::Image(_)) && self.extras().colorize.is_some() && colorize(self))
|
||||
|| effects(&self.appearance.effects)
|
||||
|| self
|
||||
.appearance
|
||||
.items
|
||||
.iter()
|
||||
.any(|i| i.visible() && (i.opacity() < 1.0 || i.blend() != BlendMode::Normal || see_through(i.paint()) || effects(i.effects())))
|
||||
|| self.children().is_some_and(|ch| ch.iter().any(|c| c.shows_transparency_with(colorize))))
|
||||
self.opacity < 1.0
|
||||
|| self.blend != BlendMode::Normal
|
||||
|| self.extras().mask.as_ref().is_some_and(|m| !m.disabled)
|
||||
|| (matches!(self.kind, NodeKind::Image(_)) && self.extras().colorize.is_some() && colorize(self))
|
||||
|| effects(&self.appearance.effects)
|
||||
|| self
|
||||
.appearance
|
||||
.items
|
||||
.iter()
|
||||
.any(|i| i.visible() && (i.opacity() < 1.0 || i.blend() != BlendMode::Normal || see_through(i.paint()) || effects(i.effects())))
|
||||
// Characters' own fills and strokes, which draw with the object.
|
||||
|| matches!(&self.kind, NodeKind::Text(t) if t.runs.iter().any(|r| see_through(&r.style.fill) || (r.style.stroke_width > 0.0 && see_through(&r.style.stroke))))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
//! Separated images: an image whose inks come as one plate each (a DCS file placed linked), kept
|
||||
//! as a TIFF of several images. The first is the composite, the plates mixed into CMYK
|
||||
//! ([`Separated::composite`]), which every reader of a CMYK TIFF sees. Each later image is one
|
||||
//! plate's ink amounts in grayscale (0 is no ink), with its ink's name and CMYK equivalent in its
|
||||
//! ImageDescription. Print separations take each plate from these ([`Separated::coverage`]).
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_color::cms::PROCESS_PLATES;
|
||||
use vectorcraft_geom::PathData;
|
||||
|
||||
use crate::ImageBlob;
|
||||
use crate::cmyk::{Inks, MAX_CMYK_PIXELS};
|
||||
|
||||
/// Most plates a separated image holds.
|
||||
pub const MAX_PLATES: usize = 64;
|
||||
/// Most directories a separated image's TIFF holds: its composite and its plates.
|
||||
const MAX_DIRECTORIES: usize = MAX_PLATES + 1;
|
||||
/// Longest ink name kept (bytes).
|
||||
pub const MAX_NAME: usize = 255;
|
||||
/// Most ink amounts the plates of one image hold together (512 MB).
|
||||
pub const MAX_SAMPLES: u64 = 1 << 29;
|
||||
/// How a plate's ImageDescription starts.
|
||||
const MARK: &str = "VectorCraft plate ";
|
||||
|
||||
/// How the composite's ImageDescription starts when plates were read from files of their own.
|
||||
const FILES_MARK: &str = "VectorCraft plate files";
|
||||
|
||||
/// A plate file of its own that a separated image was read from, as it was then: a change to it
|
||||
/// changes the image as a change to the DCS file does.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PlateFile {
|
||||
/// Its file name, in the DCS file's folder.
|
||||
pub name: String,
|
||||
/// Its size (bytes).
|
||||
pub size: u64,
|
||||
/// Its modification time (ms since the Unix epoch; 0 when the file system keeps none).
|
||||
pub modified: u64,
|
||||
}
|
||||
|
||||
/// `text` in hexadecimal (TIFF text is ASCII).
|
||||
fn hex(text: &str) -> String {
|
||||
text.bytes().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
/// The text [`hex`] wrote, of at most `max` bytes.
|
||||
fn unhex(hex: &str, max: usize) -> Option<String> {
|
||||
if !hex.len().is_multiple_of(2) || hex.len() > max * 2 {
|
||||
return None;
|
||||
}
|
||||
let bytes: Option<Vec<u8>> = (0..hex.len()).step_by(2).map(|i| u8::from_str_radix(hex.get(i..i + 2)?, 16).ok()).collect();
|
||||
String::from_utf8(bytes?).ok()
|
||||
}
|
||||
|
||||
/// One ink's plate: its ink amounts, one byte a pixel (0 is none, 255 is solid), row by row from
|
||||
/// the top.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct InkPlate {
|
||||
/// "Cyan", "Magenta", "Yellow", "Black", or a spot ink's name.
|
||||
pub name: String,
|
||||
/// The process inks the ink stands for (C, M, Y, K, 0..1): one process ink at 100% for a
|
||||
/// process plate.
|
||||
pub cmyk: [f32; 4],
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl InkPlate {
|
||||
/// Is it a spot ink's plate (not one of the four process inks)?
|
||||
pub fn spot(&self) -> bool {
|
||||
!PROCESS_PLATES.contains(&self.name.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// The plates of a separated image, all of one size.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Separated {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub plates: Vec<InkPlate>,
|
||||
}
|
||||
|
||||
impl Separated {
|
||||
/// `plates` as a `width` × `height` image; `None` when there are none or more than
|
||||
/// [`MAX_PLATES`], two have the same name, a name is empty or longer than [`MAX_NAME`], a plate
|
||||
/// doesn't hold that many pixels, an equivalent isn't within 0..1, or the image is empty, has
|
||||
/// more than [`MAX_CMYK_PIXELS`], or its plates together hold more than [`MAX_SAMPLES`]
|
||||
/// ([`Self::fits`]). A process plate's equivalent is its own ink.
|
||||
pub fn new(width: u32, height: u32, mut plates: Vec<InkPlate>) -> Option<Self> {
|
||||
let n = u64::from(width) * u64::from(height);
|
||||
if !Self::fits(width, height, plates.len()) {
|
||||
return None;
|
||||
}
|
||||
for p in plates.iter_mut() {
|
||||
if p.name.is_empty() || p.name.len() > MAX_NAME || p.data.len() as u64 != n {
|
||||
return None;
|
||||
}
|
||||
if let Some(j) = PROCESS_PLATES.iter().position(|q| *q == p.name) {
|
||||
p.cmyk = [0.0; 4];
|
||||
p.cmyk[j] = 1.0;
|
||||
}
|
||||
if !p.cmyk.iter().all(|v| (0.0..=1.0).contains(v)) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let mut names = std::collections::HashSet::new();
|
||||
if !plates.iter().all(|p| names.insert(p.name.as_str())) {
|
||||
return None;
|
||||
}
|
||||
Some(Self { width, height, plates })
|
||||
}
|
||||
|
||||
/// May an image of `width` × `height` pixels hold `plates` plates? Not when it is empty, has
|
||||
/// more than [`MAX_CMYK_PIXELS`], has no plate or more than [`MAX_PLATES`], or the plates
|
||||
/// together would hold more than [`MAX_SAMPLES`].
|
||||
pub fn fits(width: u32, height: u32, plates: usize) -> bool {
|
||||
let n = u64::from(width) * u64::from(height);
|
||||
n > 0 && n <= MAX_CMYK_PIXELS && (1..=MAX_PLATES).contains(&plates) && n * plates as u64 <= MAX_SAMPLES
|
||||
}
|
||||
|
||||
/// The plates of spot inks.
|
||||
pub fn spots(&self) -> impl Iterator<Item = &InkPlate> {
|
||||
self.plates.iter().filter(|p| p.spot())
|
||||
}
|
||||
|
||||
/// The plates mixed into process inks: each plate's ink amount times its equivalent, the
|
||||
/// plates over one another as inks print (`1 − (1 − a)(1 − b)` for each process ink).
|
||||
pub fn composite(&self) -> Inks {
|
||||
let n = self.data_len();
|
||||
let mut data = vec![0u8; n * 4];
|
||||
for (i, px) in data.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||
let mut clear = [1.0f32; 4];
|
||||
for p in &self.plates {
|
||||
let t = f32::from(p.data[i]) / 255.0;
|
||||
for (c, e) in clear.iter_mut().zip(p.cmyk) {
|
||||
*c *= 1.0 - t * e;
|
||||
}
|
||||
}
|
||||
for (o, c) in px.iter_mut().zip(clear) {
|
||||
*o = ((1.0 - c) * 255.0).round() as u8;
|
||||
}
|
||||
}
|
||||
Inks { width: self.width, height: self.height, data }
|
||||
}
|
||||
|
||||
/// The ink amounts that print on plate `plate` (a process ink's name or a spot ink's), one byte
|
||||
/// a pixel. A spot plate for which `own` is true prints on its own plate only; every other spot
|
||||
/// plate (the job prints no plate of that name) prints as its process equivalent, mixed as
|
||||
/// [`Self::composite`] mixes it. A plate name the image has no plate for, and a spot plate that
|
||||
/// prints as process, print nothing.
|
||||
pub fn coverage(&self, plate: &str, own: &dyn Fn(&str) -> bool) -> Vec<u8> {
|
||||
self.coverage_as(plate, own, &|p| p.cmyk)
|
||||
}
|
||||
|
||||
/// [`Self::coverage`], a spot plate that prints as process printing as the process inks
|
||||
/// `equivalent` gives it (the document's swatch of its ink, when there is one).
|
||||
pub fn coverage_as(&self, plate: &str, own: &dyn Fn(&str) -> bool, equivalent: &dyn Fn(&InkPlate) -> [f32; 4]) -> Vec<u8> {
|
||||
let n = self.data_len();
|
||||
if let Some(j) = PROCESS_PLATES.iter().position(|q| *q == plate) {
|
||||
let mixed: Vec<(&InkPlate, f32)> = self
|
||||
.plates
|
||||
.iter()
|
||||
.filter(|p| !p.spot() || !own(&p.name))
|
||||
.map(|p| (p, if p.spot() { equivalent(p)[j].clamp(0.0, 1.0) } else { p.cmyk[j] }))
|
||||
.filter(|(_, e)| *e > 0.0)
|
||||
.collect();
|
||||
return (0..n)
|
||||
.map(|i| {
|
||||
let clear = mixed.iter().fold(1.0f32, |c, (p, e)| c * (1.0 - f32::from(p.data[i]) / 255.0 * e));
|
||||
((1.0 - clear) * 255.0).round() as u8
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
match self.plates.iter().find(|p| p.name == plate && p.spot() && own(&p.name)) {
|
||||
Some(p) => p.data.clone(),
|
||||
None => vec![0; n],
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::coverage_as`] as a grayscale PNG, black for solid ink and white for none, as a plate
|
||||
/// prints; `None` when it can't be written.
|
||||
pub fn plate_png(&self, plate: &str, own: &dyn Fn(&str) -> bool, equivalent: &dyn Fn(&InkPlate) -> [f32; 4]) -> Option<ImageBlob> {
|
||||
let gray: Vec<u8> = self.coverage_as(plate, own, equivalent).into_iter().map(|v| 255 - v).collect();
|
||||
let img = image::GrayImage::from_raw(self.width, self.height, gray)?;
|
||||
let mut png = Vec::new();
|
||||
img.write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png).ok()?;
|
||||
Some(ImageBlob::png(png))
|
||||
}
|
||||
|
||||
fn data_len(&self) -> usize {
|
||||
self.width as usize * self.height as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// A plate's ImageDescription: the mark, its equivalent and its name in hexadecimal (TIFF text is
|
||||
/// ASCII).
|
||||
fn description(p: &InkPlate) -> String {
|
||||
let [c, m, y, k] = p.cmyk;
|
||||
format!("{MARK}{c} {m} {y} {k} {}", hex(&p.name))
|
||||
}
|
||||
|
||||
/// A plate's name and equivalent from its ImageDescription; `None` when it isn't one.
|
||||
fn parse_description(s: &str) -> Option<(String, [f32; 4])> {
|
||||
let rest = s.strip_prefix(MARK)?;
|
||||
let mut words = rest.split(' ');
|
||||
let mut cmyk = [0.0f32; 4];
|
||||
for v in &mut cmyk {
|
||||
*v = words.next()?.parse().ok().filter(|v: &f32| v.is_finite())?;
|
||||
}
|
||||
let name = unhex(words.next()?, MAX_NAME)?;
|
||||
words.next().is_none().then_some((name, cmyk))
|
||||
}
|
||||
|
||||
/// Longest plate file name kept (bytes).
|
||||
const MAX_FILE_NAME: usize = 4096;
|
||||
|
||||
/// The composite's ImageDescription line for `files`: the mark, then each file's name in
|
||||
/// hexadecimal, its size and its modification time.
|
||||
fn files_description(files: &[PlateFile]) -> String {
|
||||
let mut out = String::from(FILES_MARK);
|
||||
for f in files.iter().take(MAX_PLATES) {
|
||||
out.push_str(&format!(" {} {} {}", hex(&f.name), f.size, f.modified));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// How the composite's ImageDescription line holding the clipping path starts.
|
||||
const CLIP_MARK: &str = "VectorCraft clip ";
|
||||
|
||||
/// Longest clipping path kept (bytes of its JSON).
|
||||
const MAX_CLIP: usize = 16 << 20;
|
||||
|
||||
/// The composite's ImageDescription line for clipping path `clip`: the mark, then the path's JSON
|
||||
/// in hexadecimal; `None` when it is longer than [`MAX_CLIP`].
|
||||
fn clip_description(clip: &PathData) -> Option<String> {
|
||||
let json = serde_json::to_string(clip).ok().filter(|j| j.len() <= MAX_CLIP)?;
|
||||
Some(format!("{CLIP_MARK}{}", hex(&json)))
|
||||
}
|
||||
|
||||
/// The plate files of a composite's ImageDescription; none when it isn't one.
|
||||
fn parse_files(s: &str) -> Vec<PlateFile> {
|
||||
let Some(rest) = s.lines().find_map(|l| l.strip_prefix(FILES_MARK)) else { return vec![] };
|
||||
let words: Vec<&str> = rest.split(' ').filter(|w| !w.is_empty()).collect();
|
||||
let file =
|
||||
|w: &[&str]| Some(PlateFile { name: unhex(w.first()?, MAX_FILE_NAME)?, size: w.get(1)?.parse().ok()?, modified: w.get(2)?.parse().ok()? });
|
||||
words.chunks(3).take(MAX_PLATES).filter_map(file).collect()
|
||||
}
|
||||
|
||||
impl ImageBlob {
|
||||
/// A TIFF of separated image `s`: its composite in CMYK, then its plates (Deflate-compressed),
|
||||
/// declaring the resolution `ppi` when it is given and both are finite and above 0, and noting
|
||||
/// `files`, the plate files of their own the plates were read from ([`Self::plate_files`]);
|
||||
/// `None` when it can't be written.
|
||||
pub fn separated_tiff(s: &Separated, ppi: Option<(f64, f64)>, files: &[PlateFile]) -> Option<Self> {
|
||||
Self::separated_tiff_clipped(s, ppi, files, None)
|
||||
}
|
||||
|
||||
/// [`Self::separated_tiff`], noting `clip` too, the clipping path its file draws it through, in
|
||||
/// its pixel space ([`Self::separated_clip`]).
|
||||
pub fn separated_tiff_clipped(s: &Separated, ppi: Option<(f64, f64)>, files: &[PlateFile], clip: Option<&PathData>) -> Option<Self> {
|
||||
use tiff::encoder::{Compression, Rational, TiffEncoder, colortype, compression::DeflateLevel};
|
||||
use tiff::tags::{ResolutionUnit, Tag};
|
||||
let ppi = ppi.filter(|(x, y)| [x, y].iter().all(|v| v.is_finite() && **v > 0.0));
|
||||
// Ten-thousandths of a pixel per inch.
|
||||
let r = |v: f64| Rational { n: (v * 10_000.0).round() as u32, d: 10_000 };
|
||||
let mut out = Cursor::new(Vec::new());
|
||||
let mut enc = TiffEncoder::new(&mut out).ok()?.with_compression(Compression::Deflate(DeflateLevel::Balanced));
|
||||
let mut img = enc.new_image::<colortype::CMYK8>(s.width, s.height).ok()?;
|
||||
if let Some((x, y)) = ppi {
|
||||
img.encoder().write_tag(Tag::XResolution, r(x)).ok()?;
|
||||
img.encoder().write_tag(Tag::YResolution, r(y)).ok()?;
|
||||
img.encoder().write_tag(Tag::ResolutionUnit, ResolutionUnit::Inch).ok()?;
|
||||
}
|
||||
// One line for the plate files, one for the clipping path.
|
||||
let lines: Vec<String> =
|
||||
[(!files.is_empty()).then(|| files_description(files)), clip.and_then(clip_description)].into_iter().flatten().collect();
|
||||
if !lines.is_empty() {
|
||||
img.encoder().write_tag(Tag::ImageDescription, lines.join("\n").as_str()).ok()?;
|
||||
}
|
||||
img.write_data(&s.composite().data).ok()?;
|
||||
for p in &s.plates {
|
||||
// White is no ink, so a viewer that shows a plate shows it as it prints.
|
||||
let mut img = enc.new_image::<colortype::Gray8>(s.width, s.height).ok()?;
|
||||
img.encoder().write_tag(Tag::ImageDescription, description(p).as_str()).ok()?;
|
||||
let inverted: Vec<u8> = p.data.iter().map(|v| 255 - v).collect();
|
||||
img.write_data(&inverted).ok()?;
|
||||
}
|
||||
Some(Self::new("image/tiff", out.into_inner()))
|
||||
}
|
||||
|
||||
/// Does the image hold plates ([`ImageBlob::separated_tiff`]), at most [`MAX_PLATES`] of them?
|
||||
/// One with more, or with more directories than its composite and [`MAX_PLATES`] plates, is the
|
||||
/// CMYK image of its composite only, as [`Self::separated`] refuses it. Reads only the TIFF's
|
||||
/// directories, at most [`MAX_DIRECTORIES`].
|
||||
pub fn is_separated(&self) -> bool {
|
||||
let Some((_, mut d)) = crate::cmyk::tiff(&self.bytes) else { return false };
|
||||
let (mut plates, mut directories) = (0usize, 1usize);
|
||||
while d.more_images() {
|
||||
directories += 1;
|
||||
if directories > MAX_DIRECTORIES || d.next_image().is_err() {
|
||||
return false;
|
||||
}
|
||||
if d.get_tag_ascii_string(tiff::tags::Tag::ImageDescription).ok().is_some_and(|s| s.starts_with(MARK)) {
|
||||
plates += 1;
|
||||
if plates > MAX_PLATES {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
plates > 0
|
||||
}
|
||||
|
||||
/// The plate files of their own a separated image was read from, as they were then; none for
|
||||
/// other images. Reads only the TIFF's first directory.
|
||||
pub fn plate_files(&self) -> Vec<PlateFile> {
|
||||
let Some((_, mut d)) = crate::cmyk::tiff(&self.bytes) else { return vec![] };
|
||||
d.get_tag_ascii_string(tiff::tags::Tag::ImageDescription).ok().as_deref().map(parse_files).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The clipping path a separated image's file draws it through, in its pixel space, as
|
||||
/// [`Self::separated_tiff_clipped`] noted it; `None` for other images. Reads only the TIFF's
|
||||
/// first directory.
|
||||
pub fn separated_clip(&self) -> Option<PathData> {
|
||||
let (_, mut d) = crate::cmyk::tiff(&self.bytes)?;
|
||||
let text = d.get_tag_ascii_string(tiff::tags::Tag::ImageDescription).ok()?;
|
||||
let json = unhex(text.lines().find_map(|l| l.strip_prefix(CLIP_MARK))?, MAX_CLIP)?;
|
||||
serde_json::from_str(&json).ok()
|
||||
}
|
||||
|
||||
/// The spot inks a separated image has plates for, each with its CMYK equivalent; none for
|
||||
/// other images. Reads only the TIFF's directories.
|
||||
pub fn spot_plates(&self) -> Vec<(String, [f32; 4])> {
|
||||
if !self.is_separated() {
|
||||
return vec![];
|
||||
}
|
||||
let Some((_, mut d)) = crate::cmyk::tiff(&self.bytes) else { return vec![] };
|
||||
let mut inks = vec![];
|
||||
while d.more_images() && inks.len() < MAX_PLATES {
|
||||
if d.next_image().is_err() {
|
||||
break;
|
||||
}
|
||||
let plate = d.get_tag_ascii_string(tiff::tags::Tag::ImageDescription).ok().as_deref().and_then(parse_description);
|
||||
inks.extend(plate.filter(|(name, _)| !PROCESS_PLATES.contains(&name.as_str())));
|
||||
}
|
||||
inks
|
||||
}
|
||||
|
||||
/// The plates of a separated image; `None` for other images, and ones whose plates can't be
|
||||
/// read. The plates of the last image read are kept for the next call (print separates one
|
||||
/// image plate after plate).
|
||||
pub fn separated(&self) -> Option<Arc<Separated>> {
|
||||
thread_local! {
|
||||
static LAST: std::cell::RefCell<Option<(String, Arc<Separated>)>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
if !self.is_separated() {
|
||||
return None;
|
||||
}
|
||||
let key = self.content_key();
|
||||
LAST.with_borrow_mut(|last| match last {
|
||||
Some((k, s)) if *k == key => Some(s.clone()),
|
||||
_ => {
|
||||
let s = Arc::new(read_plates(&self.bytes)?);
|
||||
*last = Some((key, s.clone()));
|
||||
Some(s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The plates of TIFF `bytes`, each the size of its first image.
|
||||
fn read_plates(bytes: &[u8]) -> Option<Separated> {
|
||||
let (_, mut d) = crate::cmyk::tiff(bytes)?;
|
||||
let (w, h) = d.dimensions().ok()?;
|
||||
let n = usize::try_from(u64::from(w) * u64::from(h)).ok()?;
|
||||
let mut plates = vec![];
|
||||
// Every plate is read: a file with more than [`MAX_PLATES`] is refused ([`Separated::fits`]),
|
||||
// not cut short, and so is one with more than [`MAX_DIRECTORIES`] directories.
|
||||
let mut directories = 1usize;
|
||||
while d.more_images() {
|
||||
directories += 1;
|
||||
if directories > MAX_DIRECTORIES {
|
||||
return None;
|
||||
}
|
||||
d.next_image().ok()?;
|
||||
let Some((name, cmyk)) = d.get_tag_ascii_string(tiff::tags::Tag::ImageDescription).ok().as_deref().and_then(parse_description) else {
|
||||
continue;
|
||||
};
|
||||
// Checked before the plate is decoded.
|
||||
if d.dimensions().ok()? != (w, h) || !matches!(d.colortype().ok()?, tiff::ColorType::Gray(8)) || !Separated::fits(w, h, plates.len() + 1) {
|
||||
return None;
|
||||
}
|
||||
let data = crate::cmyk::read_samples(&mut d, n, 1)?.into_iter().map(|v| 255 - v).collect();
|
||||
plates.push(InkPlate { name, cmyk, data });
|
||||
}
|
||||
Separated::new(w, h, plates)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn plate(name: &str, cmyk: [f32; 4], data: &[u8]) -> InkPlate {
|
||||
InkPlate { name: name.into(), cmyk, data: data.to_vec() }
|
||||
}
|
||||
|
||||
/// Two pixels: cyan 100% and a spot ink at 50% on the first, nothing on the second.
|
||||
fn two() -> Separated {
|
||||
let plates = vec![plate("Cyan", [0.0; 4], &[255, 0]), plate("Spot Orange", [0.0, 0.6, 1.0, 0.0], &[128, 0])];
|
||||
Separated::new(2, 1, plates).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plates_mix_into_the_composite_as_inks_print() {
|
||||
let s = two();
|
||||
assert_eq!(s.plates[0].cmyk, [1.0, 0.0, 0.0, 0.0], "a process plate stands for its own ink");
|
||||
let c = s.composite();
|
||||
// Magenta: 0.6 × 128/255; yellow: 128/255.
|
||||
assert_eq!(&c.data[..4], &[255, 77, 128, 0]);
|
||||
assert_eq!(&c.data[4..], &[0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_spot_plate_prints_on_its_own_plate_or_as_process() {
|
||||
let s = two();
|
||||
let own = |_: &str| true;
|
||||
assert_eq!(s.coverage("Spot Orange", &own), [128, 0]);
|
||||
assert_eq!(s.coverage("Magenta", &own), [0, 0], "nothing of the spot on a process plate");
|
||||
assert_eq!(s.coverage("Cyan", &own), [255, 0]);
|
||||
assert_eq!(s.coverage("Black", &own), [0, 0]);
|
||||
let process = |_: &str| false;
|
||||
assert_eq!(s.coverage("Spot Orange", &process), [0, 0]);
|
||||
assert_eq!(s.coverage("Magenta", &process), [77, 0]);
|
||||
assert_eq!(s.coverage("Yellow", &process), [128, 0]);
|
||||
assert_eq!(s.coverage("Other", &own), [0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tiff_keeps_every_plate_and_shows_the_composite() {
|
||||
let s = two();
|
||||
let files =
|
||||
[PlateFile { name: "pic (1).C".into(), size: 1234, modified: 1_700_000_000_000 }, PlateFile { name: "ü.K".into(), size: 0, modified: 0 }];
|
||||
let b = ImageBlob::separated_tiff(&s, Some((300.0, 300.0)), &files).unwrap();
|
||||
assert_eq!(b.plate_files(), files);
|
||||
assert!(ImageBlob::separated_tiff(&s, None, &[]).unwrap().plate_files().is_empty());
|
||||
assert!(b.is_separated());
|
||||
assert!(b.is_cmyk(), "readers of a CMYK TIFF see the composite");
|
||||
assert_eq!(b.cmyk(), Some(s.composite()));
|
||||
assert_eq!(*b.separated().unwrap(), s);
|
||||
assert_eq!(b.spot_plates(), [("Spot Orange".to_string(), [0.0, 0.6, 1.0, 0.0])]);
|
||||
assert!(ImageBlob::cmyk_tiff(&s.composite()).unwrap().spot_plates().is_empty());
|
||||
assert!(!ImageBlob::cmyk_tiff(&s.composite()).unwrap().is_separated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tiff_keeps_the_clipping_path_beside_the_plate_files() {
|
||||
let s = two();
|
||||
let clip = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 1.5, 1.0));
|
||||
let files = [PlateFile { name: "a.C".into(), size: 1, modified: 2 }];
|
||||
let b = ImageBlob::separated_tiff_clipped(&s, None, &files, Some(&clip)).unwrap();
|
||||
assert_eq!(b.separated_clip(), Some(clip.clone()));
|
||||
assert_eq!(b.plate_files(), files);
|
||||
let alone = ImageBlob::separated_tiff_clipped(&s, None, &[], Some(&clip)).unwrap();
|
||||
assert_eq!(alone.separated_clip(), Some(clip));
|
||||
assert!(alone.plate_files().is_empty());
|
||||
assert_eq!(ImageBlob::separated_tiff(&s, None, &files).unwrap().separated_clip(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tiff_with_more_plates_than_are_read_is_refused() {
|
||||
use tiff::encoder::{TiffEncoder, colortype};
|
||||
let tiff = |plates: usize| {
|
||||
let mut out = Cursor::new(Vec::new());
|
||||
let mut enc = TiffEncoder::new(&mut out).unwrap();
|
||||
enc.write_image::<colortype::CMYK8>(1, 1, &[0, 0, 0, 0]).unwrap();
|
||||
for i in 0..plates {
|
||||
let mut img = enc.new_image::<colortype::Gray8>(1, 1).unwrap();
|
||||
img.encoder().write_tag(tiff::tags::Tag::ImageDescription, description(&plate(&format!("S{i}"), [0.0; 4], &[0])).as_str()).unwrap();
|
||||
img.write_data(&[255]).unwrap();
|
||||
}
|
||||
ImageBlob::new("image/tiff", out.into_inner())
|
||||
};
|
||||
assert_eq!(tiff(MAX_PLATES).separated().map(|s| s.plates.len()), Some(MAX_PLATES));
|
||||
let over = tiff(MAX_PLATES + 1);
|
||||
assert!(over.separated().is_none(), "not cut short to the first {MAX_PLATES}");
|
||||
assert!(!over.is_separated() && over.spot_plates().is_empty(), "the CMYK image of its composite only");
|
||||
assert!(tiff(MAX_PLATES).is_separated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tiff_with_more_directories_than_a_composite_and_its_plates_is_refused() {
|
||||
use tiff::encoder::{TiffEncoder, colortype};
|
||||
let tiff = |others: usize| {
|
||||
let mut out = Cursor::new(Vec::new());
|
||||
let mut enc = TiffEncoder::new(&mut out).unwrap();
|
||||
enc.write_image::<colortype::CMYK8>(1, 1, &[0, 0, 0, 0]).unwrap();
|
||||
let mut img = enc.new_image::<colortype::Gray8>(1, 1).unwrap();
|
||||
img.encoder().write_tag(tiff::tags::Tag::ImageDescription, description(&plate("S", [0.0; 4], &[0])).as_str()).unwrap();
|
||||
img.write_data(&[255]).unwrap();
|
||||
// Directories that are not plates.
|
||||
for _ in 0..others {
|
||||
enc.write_image::<colortype::Gray8>(1, 1, &[255]).unwrap();
|
||||
}
|
||||
ImageBlob::new("image/tiff", out.into_inner())
|
||||
};
|
||||
let most = tiff(MAX_PLATES - 1);
|
||||
assert!(most.is_separated() && most.separated().is_some());
|
||||
let over = tiff(MAX_PLATES);
|
||||
assert!(!over.is_separated() && over.separated().is_none() && over.spot_plates().is_empty());
|
||||
assert!(read_plates(&over.bytes).is_none(), "read_plates stops at the limit too");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_that_are_not_ascii_are_kept() {
|
||||
let s = Separated::new(1, 1, vec![plate("Grün 100 % (A)", [0.5, 0.0, 1.0, 0.0], &[9])]).unwrap();
|
||||
let b = ImageBlob::separated_tiff(&s, None, &[]).unwrap();
|
||||
assert_eq!(b.separated().unwrap().plates[0].name, "Grün 100 % (A)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_plates_are_refused() {
|
||||
assert!(Separated::new(2, 1, vec![]).is_none(), "no plates");
|
||||
assert!(Separated::new(2, 1, vec![plate("Cyan", [0.0; 4], &[1])]).is_none(), "too few pixels");
|
||||
assert!(Separated::new(1, 1, vec![plate("A", [0.0; 4], &[1]), plate("A", [0.0; 4], &[1])]).is_none(), "the same name twice");
|
||||
assert!(Separated::new(1, 1, vec![plate("", [0.0; 4], &[1])]).is_none(), "no name");
|
||||
assert!(Separated::new(1, 1, vec![plate("A", [2.0, 0.0, 0.0, 0.0], &[1])]).is_none(), "equivalent past 100%");
|
||||
assert!(Separated::new(0, 1, vec![plate("A", [0.0; 4], &[])]).is_none(), "empty");
|
||||
let many: Vec<InkPlate> = (0..=MAX_PLATES).map(|i| plate(&format!("S{i}"), [0.0; 4], &[1])).collect();
|
||||
assert!(Separated::new(1, 1, many).is_none(), "too many plates");
|
||||
assert!(Separated::fits(1 << 13, 1 << 13, 8), "512 MB of ink amounts");
|
||||
assert!(!Separated::fits(1 << 13, 1 << 13, 9), "more than 512 MB together");
|
||||
assert!(!Separated::fits(1 << 14, 1 << 13, 1), "more pixels than a CMYK image is decoded with");
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ vectorcraft-doc = { workspace = true }
|
||||
vectorcraft-pathops = { workspace = true }
|
||||
vectorcraft-text = { workspace = true }
|
||||
vectorcraft-plugins = { workspace = true }
|
||||
vectorcraft-trace = { workspace = true }
|
||||
image = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
# Effect › SVG Filters draws its filters with resvg (MIT OR Apache-2.0), at the usvg version the
|
||||
|
||||
@@ -296,7 +296,7 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
"path.outlineObject",
|
||||
"Outline Object",
|
||||
PATH,
|
||||
"{} type is drawn, exported and expanded as its glyph outlines and stays editable, and the effects after it reshape those outlines; paths and images do not change",
|
||||
"{} type is drawn, exported and expanded as its glyph outlines and stays editable, and the effects after it reshape those outlines; paths and images do not change. On an image's own fill or stroke it gives the outline of the image's pixels that are at least half opaque (VectorCraft's own cutoff), and nothing when no pixel is; it gives the image's frame when the image has no transparency, its pixels can't be read, or the image or its outline is too large to trace",
|
||||
json!({}),
|
||||
),
|
||||
g(
|
||||
|
||||
@@ -80,8 +80,8 @@ 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, 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_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,
|
||||
|
||||
+133
-15
@@ -10,12 +10,13 @@
|
||||
//! inline graphics whose own fills or strokes have geometry effects is outlined too: each of those
|
||||
//! effects reshapes the whole outline that its fill or stroke paints. An image's own fills and
|
||||
//! strokes paint only the shape that a Convert to Shape or Outline Object effect on them gives
|
||||
//! them ([`image_rows`]).
|
||||
//! them ([`image_rows`]): Outline Object follows the outline of the image's opaque area
|
||||
//! ([`opaque_area`]).
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock, PoisonError, Weak};
|
||||
|
||||
use vectorcraft_doc::live::Hooks;
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, ImageObject, Node, NodeId, NodeKind};
|
||||
@@ -287,8 +288,8 @@ pub fn gives_image_shape(effects: &[Effect]) -> bool {
|
||||
|
||||
/// The fills and strokes of `items` (an image's own) that paint something ([`shapes_image`]), each
|
||||
/// without the effects before its first Convert to Shape or Outline Object. Convert to Shape sizes
|
||||
/// its shape from the image's box, and Outline Object gives the image's frame. The others paint
|
||||
/// nothing.
|
||||
/// its shape from the image's box, and Outline Object gives the outline of the image's opaque
|
||||
/// area ([`opaque_area`]), or its frame when it has no transparency. The others paint nothing.
|
||||
fn image_rows(items: &[AppearanceItem]) -> Vec<AppearanceItem> {
|
||||
items
|
||||
.iter()
|
||||
@@ -303,11 +304,109 @@ fn image_rows(items: &[AppearanceItem]) -> Vec<AppearanceItem> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Does `item`, one of [`image_rows`], start from the outline of the image's opaque area (is its
|
||||
/// first effect Outline Object rather than Convert to Shape)?
|
||||
fn follows_opaque_area(item: &AppearanceItem) -> bool {
|
||||
item.effects().first().is_some_and(|e| e.id == "path.outlineObject")
|
||||
}
|
||||
|
||||
/// Is `n` an image with a fill or stroke of its own that follows the outline of the image's opaque
|
||||
/// area ([`image_rows`], [`opaque_area`])? The shape that fill or stroke paints then changes with
|
||||
/// the image's pixels in the document while `n` stays the same.
|
||||
pub fn follows_image_pixels(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Image(_))
|
||||
&& n.appearance
|
||||
.items
|
||||
.iter()
|
||||
.any(|i| shapes_image(i) && i.effects().iter().find(|e| gives_shape(e)).is_some_and(|e| e.id == "path.outlineObject"))
|
||||
}
|
||||
|
||||
/// The outline of a set of pixels' opaque area, in the pixel space they decode to, with that size
|
||||
/// ([`traced_opaque_area`]). `None` for pixels that have no transparency or could not be traced.
|
||||
type Traced = Option<(Arc<PathData>, (u32, u32))>;
|
||||
|
||||
/// One of [`Outlines`]' entries: the pixels, held weakly, and their outline once it is traced.
|
||||
type Entry = (Weak<Vec<u8>>, Arc<OnceLock<Traced>>);
|
||||
|
||||
/// The outlines [`opaque_area`] has traced, by the address of the pixels each was traced from.
|
||||
struct Outlines {
|
||||
/// Each outline with its pixels. An entry does not keep its pixels, and while it is kept no
|
||||
/// other pixels can take their address. Entries whose pixels are gone are dropped before a
|
||||
/// new outline is traced and every [`PRUNE_EVERY`] lookups.
|
||||
kept: BTreeMap<usize, Entry>,
|
||||
/// The lookups since the last pruning.
|
||||
lookups: usize,
|
||||
}
|
||||
|
||||
static OUTLINES: Mutex<Outlines> = Mutex::new(Outlines { kept: BTreeMap::new(), lookups: 0 });
|
||||
|
||||
/// How many lookups [`OUTLINES`] takes between two prunings of its entries for pixels that are gone.
|
||||
const PRUNE_EVERY: usize = 256;
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// The outlines [`traced_opaque_area`] has traced on this thread.
|
||||
pub(crate) static TRACES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// Does [`OUTLINES`] keep an entry for `pixels`?
|
||||
#[cfg(test)]
|
||||
pub(crate) fn keeps_outline(pixels: &Weak<Vec<u8>>) -> bool {
|
||||
OUTLINES.lock().unwrap_or_else(PoisonError::into_inner).kept.values().any(|(w, _)| w.ptr_eq(pixels))
|
||||
}
|
||||
|
||||
/// The outline of the opaque area of the image `im` in the document
|
||||
/// ([`vectorcraft_trace::opaque_outline`]: its pixels that are at least half opaque, outlined with
|
||||
/// fitted curves), its pixels taken from `im` itself or from the first of `docs` that holds them.
|
||||
/// `None` when the image has no transparency, its pixels can't be read or are too large to read
|
||||
/// ([`vectorcraft_trace::decode_alpha`]), or its outline would be too complex: Outline Object then
|
||||
/// follows the image's frame. The outline is traced once for each set of pixels and kept while
|
||||
/// those pixels are: a caller that asks while another thread traces the same pixels waits for that
|
||||
/// trace.
|
||||
pub(crate) fn opaque_area(docs: &[Option<&Document>], im: &ImageObject) -> Option<PathData> {
|
||||
let blob = im.pixels.as_deref().or_else(|| docs.iter().flatten().find_map(|d| d.images.get(&im.key)))?;
|
||||
let bytes = &blob.bytes;
|
||||
let cell = {
|
||||
let mut o = OUTLINES.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
o.lookups += 1;
|
||||
if o.lookups >= PRUNE_EVERY {
|
||||
o.lookups = 0;
|
||||
o.kept.retain(|_, (w, _)| w.strong_count() > 0);
|
||||
}
|
||||
let key = Arc::as_ptr(bytes).addr();
|
||||
match o.kept.get(&key) {
|
||||
Some((_, cell)) => Arc::clone(cell),
|
||||
None => {
|
||||
o.kept.retain(|_, (w, _)| w.strong_count() > 0);
|
||||
let cell = Arc::new(OnceLock::new());
|
||||
o.kept.insert(key, (Arc::downgrade(bytes), Arc::clone(&cell)));
|
||||
cell
|
||||
}
|
||||
}
|
||||
};
|
||||
// Traced without the lock: another image's outline need not wait for this one.
|
||||
let (path, (w, h)) = cell.get_or_init(|| traced_opaque_area(bytes)).clone()?;
|
||||
let to_image = Affine::scale_non_uniform(f64::from(im.width) / f64::from(w.max(1)), f64::from(im.height) / f64::from(h.max(1)));
|
||||
Some(path.transformed(im.xf * to_image))
|
||||
}
|
||||
|
||||
/// [`opaque_area`]'s outline of the encoded image `bytes`. An image whose format has no alpha is
|
||||
/// not decoded.
|
||||
fn traced_opaque_area(bytes: &[u8]) -> Traced {
|
||||
#[cfg(test)]
|
||||
TRACES.with(|t| t.set(t.get() + 1));
|
||||
let (w, h, alpha) = vectorcraft_trace::decode_alpha(bytes).ok()??;
|
||||
let outline = vectorcraft_trace::opaque_outline(w, h, alpha).ok()??;
|
||||
Some((Arc::new(outline), (w, h)))
|
||||
}
|
||||
|
||||
/// The shapes that the image `n`'s own fills and strokes paint ([`image_rows`]), each with its fill
|
||||
/// rule and the fill or stroke that paints it: the paths (or compound paths) along its frame that
|
||||
/// [`reshape`] makes, with each fill's or stroke's geometry effects applied as the renderer applies
|
||||
/// them. Empty for anything but an image with such fills or strokes. The swatches and patterns that
|
||||
/// live-effect plug-ins painting their pieces name are looked up in `doc`.
|
||||
/// rule and the fill or stroke that paints it: the paths (or compound paths) that [`reshape`]
|
||||
/// makes along its frame or, for those whose first effect is Outline Object, along the outline of
|
||||
/// its opaque area ([`opaque_area`]), with each fill's or stroke's geometry effects applied as the
|
||||
/// renderer applies them. Empty for anything but an image with such fills or strokes. The swatches
|
||||
/// and patterns that live-effect plug-ins painting their pieces name are looked up in `doc`, and
|
||||
/// the image's pixels in `doc` when the image does not hold them.
|
||||
pub fn image_paint(doc: Option<&Document>, n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> {
|
||||
if !matches!(n.kind, NodeKind::Image(_)) || !has_item_geometry(n) {
|
||||
return vec![];
|
||||
@@ -608,9 +707,10 @@ pub(crate) fn outline_nested_text(doc: Option<&Document>, n: &mut Node, clipping
|
||||
/// `n` (see [`needs_outline`]) with its object-level geometry effects and its perspective applied:
|
||||
/// its [`outline_art`] reshaped as one piece, or for an image the image clipped by its reshaped
|
||||
/// frame. Type's own fills and strokes keep their effects on its whole outline. An image's own
|
||||
/// fills and strokes that paint a shape ([`image_rows`]) become paths along its frame below and
|
||||
/// above it, 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
|
||||
/// fills and strokes that paint a shape ([`image_rows`]) become paths below and above it, along
|
||||
/// its frame or, from Outline Object, along the outline of its opaque area ([`opaque_area`]),
|
||||
/// 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
|
||||
@@ -697,15 +797,33 @@ fn as_art_opts(
|
||||
}
|
||||
Node::new(n.id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(image.clone())], clip: true })
|
||||
};
|
||||
// The fills and strokes that paint a shape, as paths along the frame below and above
|
||||
// the image.
|
||||
// The fills and strokes that paint a shape, as paths below and above the image: along
|
||||
// the frame, or for those whose first effect is Outline Object along the outline of
|
||||
// the image's opaque area (traced only when one of them needs it). A run of either
|
||||
// kind is one path; a fill or stroke whose outline is empty (an image with no opaque
|
||||
// pixel) paints nothing.
|
||||
let (below, above) = n.appearance.split_contents();
|
||||
let (below, above) = (image_rows(below), image_rows(above));
|
||||
if below.is_empty() && above.is_empty() {
|
||||
clipped()
|
||||
} else {
|
||||
let opaque = std::cell::OnceCell::new();
|
||||
let rows = |items: Vec<AppearanceItem>| {
|
||||
(!items.is_empty()).then(|| Arc::new(Node::path(n.id, frame.clone(), Appearance { items, ..Default::default() })))
|
||||
let mut out = vec![];
|
||||
let mut items = items.into_iter().peekable();
|
||||
while let Some(first) = items.next() {
|
||||
let follows = follows_opaque_area(&first);
|
||||
let mut run = vec![first];
|
||||
while let Some(next) = items.next_if(|i| follows_opaque_area(i) == follows) {
|
||||
run.push(next);
|
||||
}
|
||||
let path = if follows { opaque.get_or_init(|| opaque_area(&[symbols, paint_doc], im)).as_ref() } else { None };
|
||||
let path = path.unwrap_or(&frame);
|
||||
if !path.is_empty() {
|
||||
out.push(Arc::new(Node::path(n.id, path.clone(), Appearance { items: run, ..Default::default() })));
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
// Without geometry effects, the image is not clipped.
|
||||
let image = if fx.is_empty() { image.clone() } else { clipped() };
|
||||
|
||||
@@ -866,7 +866,8 @@ fn masks_surface(paint: &Paint, colored: bool) -> bool {
|
||||
}
|
||||
|
||||
/// The graphics of the solids `filled` ([`graphic_art`]), each with the index of the surface it is
|
||||
/// drawn right after (the last of those solids holding its face) and its face. With `per_member`
|
||||
/// drawn right after (the last of those solids holding its face) and its face. Solids in a run
|
||||
/// with the same opacity and blending mode share their graphics. With `per_member`
|
||||
/// (a member's opacity mask or blending mode applies to its surfaces) each member's solids get
|
||||
/// graphics of their own, clipped to that member's caps, so they stay in that member's scopes.
|
||||
/// All the groups share one limit on the path elements drawn: once it is reached, later groups
|
||||
@@ -883,11 +884,12 @@ fn placed_graphics(
|
||||
for f in filled {
|
||||
// Solids whose fills or strokes differ in opacity or blending mode get graphics of their
|
||||
// own, each drawn as its fill or stroke is. A solid whose surface carries an opacity mask
|
||||
// gets its own too, which takes that mask ([`with_graphics`]).
|
||||
// gets its own too, which takes that mask ([`with_graphics`]). A solid joins only the
|
||||
// group of the solid before it, so a graphic is never drawn past a solid painted between.
|
||||
let joins = |first: &Filled| {
|
||||
(!per_member || first.member == f.member) && first.opacity == f.opacity && first.blend == f.blend && !first.masked && !f.masked
|
||||
};
|
||||
match groups.iter_mut().find(|g| g.first().is_some_and(|first| joins(first))) {
|
||||
match groups.last_mut().filter(|g| g.first().is_some_and(|first| joins(first))) {
|
||||
Some(g) => g.push(f),
|
||||
None => groups.push(vec![f]),
|
||||
}
|
||||
|
||||
@@ -365,6 +365,41 @@ fn members_with_their_own_blending_get_graphics_of_their_own() {
|
||||
assert_eq!(blended, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_graphic_stays_under_a_solid_painted_after_its_own() {
|
||||
let d = document();
|
||||
let member = |id: u64, r: Rect, color: Color, opacity: f32| {
|
||||
let mut n = square(id, r, Paint::solid(color));
|
||||
if let Some(AppearanceItem::Fill(f)) = n.appearance.items.first_mut() {
|
||||
f.opacity = opacity;
|
||||
}
|
||||
Arc::new(n)
|
||||
};
|
||||
let gray = Color::rgb(0.5, 0.5, 0.5);
|
||||
// A and C share their fill's opacity; B, painted between them, has its own and covers part of A.
|
||||
let a = member(1, Rect::new(0.0, 0.0, 60.0, 100.0), gray, 1.0);
|
||||
let b = member(2, Rect::new(40.0, 0.0, 100.0, 100.0), Color::rgb(0.0, 0.0, 1.0), 0.5);
|
||||
let c = member(4, Rect::new(200.0, 0.0, 260.0, 100.0), gray, 1.0);
|
||||
let mut group = Node::group(NodeId(3), vec![a, b, c]);
|
||||
let p = json!({"rotationX": 0.0, "rotationY": 0.0, "rotationZ": 0.0, "shade": false, "graphics": [{"symbol": "Mark", "scale": 3000.0}]});
|
||||
group.appearance.effects.push(crate::new_effect(EXTRUDE, &p).unwrap());
|
||||
let art = solid_art_in(Some(&d), &group).unwrap();
|
||||
// In drawing order: 'G' for a graphic, 'B' for a face of B's solid.
|
||||
let mut order = String::new();
|
||||
art.walk(&mut |n| {
|
||||
if matches!(n.kind, NodeKind::Group { clip: true, .. }) && !graphics_in(n).is_empty() {
|
||||
order.push('G');
|
||||
} else if let Some(AppearanceItem::Fill(f)) = n.appearance.items.first()
|
||||
&& f.paint.color().is_some_and(|c| c.to_rgb()[2] > 0.9 && c.to_rgb()[0] < 0.1)
|
||||
{
|
||||
order.push('B');
|
||||
}
|
||||
});
|
||||
// A's graphic is drawn right after A, under B; C's is drawn after B.
|
||||
assert_eq!(graphics_in(&art).len(), 3, "{order}");
|
||||
assert!(order.starts_with('G'), "{order}");
|
||||
}
|
||||
|
||||
/// [`document`] with symbol "Dense" too: a red polygon of 30,000 sides, straight or `curved`.
|
||||
fn dense_document(curved: bool) -> Document {
|
||||
let mut d = document();
|
||||
|
||||
@@ -4,6 +4,7 @@ use vectorcraft_geom::{Affine, PathData, Point, Rect, Shape, SubPath, shapes};
|
||||
|
||||
use super::*;
|
||||
|
||||
mod imageoutline;
|
||||
mod rasterfollow;
|
||||
mod scale;
|
||||
mod transformfx;
|
||||
@@ -886,6 +887,108 @@ fn a_hidden_path_past_the_budget_leaves_a_layers_gradients_free_to_move() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Under a Warp effect a hidden path in a layer that keeps its shape past the budget does not hold
|
||||
/// the layer's gradient in place either: it moves and scales from the layer's bounds to the warped
|
||||
/// square's bounds, as the art and inside the art.
|
||||
#[test]
|
||||
fn a_hidden_path_past_the_budget_leaves_a_layers_gradients_free_to_follow_a_warp() {
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_color::{Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop};
|
||||
use vectorcraft_doc::{AppearanceItem, FillLayer, LayerColor, NodeId, NodeKind, color::Paint};
|
||||
let stops = vec![GradientStop::new(0.0, vectorcraft_color::Color::BLACK), GradientStop::new(1.0, vectorcraft_color::Color::WHITE)];
|
||||
let geom = GradientGeom { start: (0.0, 50.0).into(), end: (100.0, 50.0).into(), aspect: 1.0, focal: None };
|
||||
let placed = Paint::Gradient(Box::new(GradientPaint { geom: Some(geom), ..GradientPaint::new(Gradient::new(GradientKind::Linear, stops)) }));
|
||||
let ends = |ap: &Appearance| match ap.fill_paint() {
|
||||
Paint::Gradient(g) => g.geom.map(|g| (g.start, g.end)),
|
||||
_ => None,
|
||||
};
|
||||
let oval = || shapes::ellipse(Rect::new(0.0, 0.0, 100.0, 100.0));
|
||||
// A layer with a visible square and a hidden circle, both 0..100, and a gradient fill of its own.
|
||||
let layer = || {
|
||||
let square = Arc::new(Node::path(NodeId(3), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::default()));
|
||||
let mut circle = Node::path(NodeId(4), oval(), Appearance::default());
|
||||
circle.visible = false;
|
||||
let mut layer = Node::layer(NodeId(2), "Layer 1", LayerColor::Preset(0));
|
||||
if let NodeKind::Layer { children, .. } = &mut layer.kind {
|
||||
*children = vec![square, Arc::new(circle)];
|
||||
}
|
||||
layer.appearance.items.push(AppearanceItem::Fill(FillLayer::new(placed.clone())));
|
||||
layer
|
||||
};
|
||||
// The box map from the layer's bounds (the square's) to `b`: (0, 50) and (100, 50) go to the
|
||||
// middles of b's sides.
|
||||
let near = |got: Option<(Point, Point)>, b: Rect| {
|
||||
let mid = (b.y0 + b.y1) / 2.0;
|
||||
got.is_some_and(|(s, e)| s.distance(Point::new(b.x0, mid)) < 1e-6 && e.distance(Point::new(b.x1, mid)) < 1e-6)
|
||||
};
|
||||
for style in ["arc", "inflate", "flag"] {
|
||||
let warp = fx(&format!("warp.{style}"), json!({"bend": 40.0, "horizontal": 20.0}));
|
||||
// Room for the warped square beside the circle as it was, and not for the warped circle
|
||||
// beside the warped square: the paths take the budget in turn, so the circle keeps its shape.
|
||||
let mut probe = layer();
|
||||
crate::reshape::reshape_leaves_within(&mut probe, &[&warp], Budget::EFFECTS, None);
|
||||
let warped = |k: usize| util::line_estimate(probe.children().unwrap()[k].path_data().unwrap());
|
||||
let circle = util::line_estimate(&oval());
|
||||
assert!(warped(1) > circle, "warp.{style}: the warped circle is drawn with more lines");
|
||||
let budget = Budget { anchors: usize::MAX, lines: warped(0) + circle };
|
||||
let mut art = layer();
|
||||
let carry = crate::reshape::reshape_leaves_within(&mut art, &[&warp], budget, None);
|
||||
assert_eq!(art.children().unwrap()[1].path_data(), Some(&oval()), "{}: the hidden circle keeps its shape", warp.id);
|
||||
let square = art.children().unwrap()[0].geometric_bounds().unwrap();
|
||||
assert!(square != Rect::new(0.0, 0.0, 100.0, 100.0), "{}: the square takes the effect: {square:?}", warp.id);
|
||||
let mut own = Appearance::clone(&art.appearance);
|
||||
carry.apply(&mut own);
|
||||
assert!(near(ends(&own), square), "{}: the layer's gradient, as the art: {:?} for {square:?}", warp.id, ends(&own));
|
||||
let mut art = Node::group(NodeId(9), vec![Arc::new(layer())]);
|
||||
crate::reshape::reshape_leaves_within(&mut art, &[&warp], budget, None);
|
||||
let got = ends(&art.children().unwrap()[0].appearance);
|
||||
assert!(near(got, square), "{}: the layer's gradient, inside the art: {got:?} for {square:?}", warp.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// A freeform gradient whose points were edited moves its points with a Warp effect from the
|
||||
/// bounds before the warp to the bounds after it, on a path, on a group inside the art and on the
|
||||
/// art itself, as a placed gradient does.
|
||||
#[test]
|
||||
fn edited_freeform_points_follow_a_warp() {
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_color::{Color, Freeform, FreeformPoint, Gradient, GradientKind, GradientPaint};
|
||||
use vectorcraft_doc::{AppearanceItem, FillLayer, NodeId, color::Paint};
|
||||
let mut ramp = GradientPaint::new(Gradient { kind: GradientKind::Freeform, ..Gradient::default() });
|
||||
let pt = |x: f64, c| FreeformPoint::new(Point::new(x, 50.0), c);
|
||||
ramp.set_freeform(Freeform { points: vec![pt(0.0, Color::BLACK), pt(100.0, Color::WHITE)], ..Default::default() });
|
||||
assert!(ramp.geom.is_none() && !ramp.is_placed());
|
||||
let freeform = Paint::Gradient(Box::new(ramp));
|
||||
let points = |ap: &Appearance| match ap.fill_paint() {
|
||||
Paint::Gradient(g) => g.freeform.as_ref().map(|f| f.points.iter().map(|p| p.at).collect::<Vec<_>>()),
|
||||
_ => None,
|
||||
};
|
||||
let square = |ap: Appearance| Arc::new(Node::path(NodeId(3), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), ap));
|
||||
// The box map from the square's bounds to `b`: (0, 50) and (100, 50) go to the middles of b's sides.
|
||||
let near = |got: Option<Vec<Point>>, b: Rect| {
|
||||
let mid = (b.y0 + b.y1) / 2.0;
|
||||
got.is_some_and(|p| p.len() == 2 && p[0].distance(Point::new(b.x0, mid)) < 1e-6 && p[1].distance(Point::new(b.x1, mid)) < 1e-6)
|
||||
};
|
||||
for style in ["arc", "inflate", "rise"] {
|
||||
let e = fx(&format!("warp.{style}"), json!({"bend": 40.0, "vertical": 20.0}));
|
||||
let mut inner = Node::group(NodeId(2), vec![square(Appearance::default())]);
|
||||
inner.appearance.items.push(AppearanceItem::Fill(FillLayer::new(freeform.clone())));
|
||||
let mut art = Node::group(NodeId(9), vec![square(Appearance::basic(freeform.clone(), Paint::None, 0.0)), Arc::new(inner)]);
|
||||
art.appearance.items.push(AppearanceItem::Fill(FillLayer::new(freeform.clone())));
|
||||
let carry = crate::reshape::reshape_leaves_within(&mut art, &[&e], Budget::EFFECTS, None);
|
||||
let b = art.geometric_bounds().unwrap();
|
||||
assert!(b != Rect::new(0.0, 0.0, 100.0, 100.0), "{}: the warp changes the bounds: {b:?}", e.id);
|
||||
let members = art.children().unwrap();
|
||||
assert!(near(points(&members[0].appearance), b), "{}: the path's points {:?} for {b:?}", e.id, points(&members[0].appearance));
|
||||
assert!(near(points(&members[1].appearance), b), "{}: the inner group's points {:?} for {b:?}", e.id, points(&members[1].appearance));
|
||||
let mut own = Appearance::clone(&art.appearance);
|
||||
carry.apply(&mut own);
|
||||
assert!(near(points(&own), b), "{}: the art's own points {:?} for {b:?}", e.id, points(&own));
|
||||
}
|
||||
}
|
||||
|
||||
/// A freeform gradient whose points were edited (its vector still fitted to the bounds) moves its
|
||||
/// points with a Transform effect and Free Distort, on a path, on a group inside the art and on the
|
||||
/// art itself, as a placed gradient does.
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
//! Outline Object on an image's own fill or stroke: the outline of the image's opaque area, for
|
||||
//! every pixel format with alpha, and the frame for one without.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{AppearanceItem, Document, Effect, FillLayer, ImageBlob, ImageObject, Node, NodeId, NodeKind, StrokeLayer};
|
||||
use vectorcraft_geom::{Affine, Rect};
|
||||
|
||||
use crate::*;
|
||||
|
||||
fn fx(id: &str) -> Effect {
|
||||
Effect { id: id.into(), params: json!({}), visible: true }
|
||||
}
|
||||
|
||||
/// `img` encoded as `format`.
|
||||
fn encoded(img: image::DynamicImage, format: image::ImageFormat) -> Vec<u8> {
|
||||
let mut out = vec![];
|
||||
img.write_to(&mut std::io::Cursor::new(&mut out), format).unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
/// A 100×100 RGBA image, transparent but for an opaque disc of radius 30 in its middle.
|
||||
fn disc() -> image::DynamicImage {
|
||||
image::DynamicImage::ImageRgba8(image::RgbaImage::from_fn(100, 100, |x, y| {
|
||||
let r = (f64::from(x) + 0.5 - 50.0).hypot(f64::from(y) + 0.5 - 50.0);
|
||||
image::Rgba([255, 0, 0, if r <= 30.0 { 255 } else { 0 }])
|
||||
}))
|
||||
}
|
||||
|
||||
/// A document holding the encoded image `bytes` (key "img") and an image object of 100×100 pixels
|
||||
/// placed at (50, 50) with the appearance items `items`, the image's own row at `contents`.
|
||||
/// → (document, the image's node).
|
||||
fn image_doc(bytes: Vec<u8>, items: Vec<AppearanceItem>, contents: usize) -> (Document, Node) {
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
d.images.insert("img".into(), ImageBlob::new("image/png", bytes));
|
||||
let mut n = Node::new(
|
||||
NodeId(0),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: "img".into(),
|
||||
width: 100,
|
||||
height: 100,
|
||||
xf: Affine::translate((50.0, 50.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
n.id = d.alloc_id();
|
||||
n.appearance.items = items;
|
||||
n.appearance.set_contents_at(contents);
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n.clone()).unwrap();
|
||||
(d, n)
|
||||
}
|
||||
|
||||
/// A PNG of `w × h` pixels with the color type `color` at `depth` bits, the chunks `extra` before
|
||||
/// its pixels, and the filtered rows `rows` stored without compression.
|
||||
fn png(w: u32, h: u32, color: u8, depth: u8, extra: &[(&[u8; 4], Vec<u8>)], rows: &[u8]) -> Vec<u8> {
|
||||
fn crc(data: &[u8]) -> u32 {
|
||||
let mut c = !0u32;
|
||||
for &b in data {
|
||||
c ^= u32::from(b);
|
||||
for _ in 0..8 {
|
||||
c = if c & 1 == 1 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 };
|
||||
}
|
||||
}
|
||||
!c
|
||||
}
|
||||
let mut out = b"\x89PNG\r\n\x1a\n".to_vec();
|
||||
let mut chunk = |kind: &[u8; 4], data: &[u8]| {
|
||||
let mut body = kind.to_vec();
|
||||
body.extend_from_slice(data);
|
||||
out.extend_from_slice(&u32::try_from(data.len()).unwrap().to_be_bytes());
|
||||
out.extend_from_slice(&body);
|
||||
out.extend_from_slice(&crc(&body).to_be_bytes());
|
||||
};
|
||||
let mut ihdr = [w.to_be_bytes(), h.to_be_bytes()].concat();
|
||||
ihdr.extend([depth, color, 0, 0, 0]);
|
||||
chunk(b"IHDR", &ihdr);
|
||||
for (kind, data) in extra {
|
||||
chunk(kind, data);
|
||||
}
|
||||
// A zlib stream of stored blocks.
|
||||
let mut z = vec![0x78, 0x01];
|
||||
let blocks: Vec<&[u8]> = if rows.is_empty() { vec![rows] } else { rows.chunks(65_535).collect() };
|
||||
for (i, block) in blocks.iter().enumerate() {
|
||||
let len = u16::try_from(block.len()).unwrap();
|
||||
z.push(u8::from(i + 1 == blocks.len()));
|
||||
z.extend(len.to_le_bytes());
|
||||
z.extend((!len).to_le_bytes());
|
||||
z.extend_from_slice(block);
|
||||
}
|
||||
let (mut a, mut b) = (1u32, 0u32);
|
||||
for &byte in rows {
|
||||
a = (a + u32::from(byte)) % 65_521;
|
||||
b = (b + a) % 65_521;
|
||||
}
|
||||
z.extend(((b << 16) | a).to_be_bytes());
|
||||
chunk(b"IDAT", &z);
|
||||
chunk(b"IEND", &[]);
|
||||
out
|
||||
}
|
||||
|
||||
fn stroke(effects: Vec<Effect>) -> AppearanceItem {
|
||||
let mut s = StrokeLayer::new(Paint::solid(Color::BLACK), 4.0);
|
||||
s.effects = effects;
|
||||
AppearanceItem::Stroke(s)
|
||||
}
|
||||
|
||||
/// The bounds of the paths the image's own fills and strokes paint, below it first.
|
||||
fn row_bounds(d: &Document, n: &Node) -> Vec<Rect> {
|
||||
let art = reshape::reshape(Some(d), n, None).unwrap();
|
||||
let mut out = vec![];
|
||||
for c in art.children().unwrap() {
|
||||
if matches!(c.kind, NodeKind::Path { .. }) {
|
||||
out.push(c.geometric_bounds().unwrap());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn near(r: Rect, want: [f64; 4], tol: f64) -> bool {
|
||||
[r.x0 - want[0], r.y0 - want[1], r.x1 - want[2], r.y1 - want[3]].iter().all(|d| d.abs() < tol)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_object_follows_the_opaque_area_of_an_image_with_alpha() {
|
||||
let gray_alpha = image::DynamicImage::ImageLumaA8(disc().to_luma_alpha8());
|
||||
let deep = image::DynamicImage::ImageRgba16(disc().to_rgba16());
|
||||
for (name, img) in [("rgba", disc()), ("gray and alpha", gray_alpha), ("16 bits a channel", deep)] {
|
||||
let (d, n) = image_doc(encoded(img, image::ImageFormat::Png), vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
let rows = row_bounds(&d, &n);
|
||||
assert_eq!(rows.len(), 1, "{name}");
|
||||
// The disc runs from 70 to 130; the frame from 50 to 150.
|
||||
assert!(near(rows[0], [70.0, 70.0, 130.0, 130.0], 0.5), "{name}: {:?}", rows[0]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_object_follows_the_frame_of_an_image_without_transparency() {
|
||||
let opaque = image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(100, 100, image::Rgb([0, 0, 255])));
|
||||
let solid = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(100, 100, image::Rgba([0, 0, 255, 200])));
|
||||
let formats = [
|
||||
("jpeg", encoded(opaque.clone(), image::ImageFormat::Jpeg)),
|
||||
("rgb png", encoded(opaque, image::ImageFormat::Png)),
|
||||
("rgba png, every pixel more than half opaque", encoded(solid, image::ImageFormat::Png)),
|
||||
];
|
||||
for (name, bytes) in formats {
|
||||
let (d, n) = image_doc(bytes, vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
let rows = row_bounds(&d, &n);
|
||||
assert!(rows.len() == 1 && near(rows[0], [50.0, 50.0, 150.0, 150.0], 1e-9), "{name}: {rows:?}");
|
||||
}
|
||||
// Pixels that can't be read keep the frame too.
|
||||
let (d, n) = image_doc(b"not an image".to_vec(), vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
assert!(near(row_bounds(&d, &n)[0], [50.0, 50.0, 150.0, 150.0], 1e-9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_to_shape_keeps_the_frame_beside_outline_object() {
|
||||
// Below the image: a fill with Convert to Shape, then a stroke with Outline Object; above it
|
||||
// a stroke with Convert to Shape before Outline Object, which starts from the frame.
|
||||
let rect = Effect { id: "convertToShape.rectangle".into(), params: json!({"relative": true, "extraW": 0, "extraH": 0}), visible: true };
|
||||
let mut fill = FillLayer::new(Paint::solid(Color::rgb(0.0, 1.0, 0.0)));
|
||||
fill.effects = vec![rect.clone()];
|
||||
let items = vec![AppearanceItem::Fill(fill), stroke(vec![fx("path.outlineObject")]), stroke(vec![rect, fx("path.outlineObject")])];
|
||||
let (d, n) = image_doc(encoded(disc(), image::ImageFormat::Png), items, 2);
|
||||
let rows = row_bounds(&d, &n);
|
||||
assert_eq!(rows.len(), 3, "{rows:?}");
|
||||
assert!(near(rows[0], [50.0, 50.0, 150.0, 150.0], 1e-9), "{rows:?}");
|
||||
assert!(near(rows[1], [70.0, 70.0, 130.0, 130.0], 0.5), "{rows:?}");
|
||||
assert!(near(rows[2], [50.0, 50.0, 150.0, 150.0], 1e-9), "{rows:?}");
|
||||
// The image keeps its place between them.
|
||||
let art = reshape::reshape(Some(&d), &n, None).unwrap();
|
||||
assert!(matches!(art.children().unwrap()[2].kind, NodeKind::Image(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_transparent_image_gives_outline_object_nothing_to_paint() {
|
||||
let clear = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(100, 100, image::Rgba([0, 0, 0, 100])));
|
||||
let (d, n) = image_doc(encoded(clear, image::ImageFormat::Png), vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
assert!(row_bounds(&d, &n).is_empty());
|
||||
let art = reshape::reshape(Some(&d), &n, None).unwrap();
|
||||
assert!(matches!(art.children().unwrap().as_slice(), [only] if matches!(only.kind, NodeKind::Image(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_outline_follows_the_images_own_pixels_and_placement() {
|
||||
// Pixels kept on the image itself (art made outside a document), placed at half size.
|
||||
let (_, mut n) = image_doc(vec![], vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
if let NodeKind::Image(im) = &mut n.kind {
|
||||
im.pixels = Some(Arc::new(ImageBlob::new("image/png", encoded(disc(), image::ImageFormat::Png))));
|
||||
im.xf = Affine::translate((10.0, 20.0)) * Affine::scale(0.5);
|
||||
}
|
||||
let art = reshape::reshape(None, &n, None).unwrap();
|
||||
let b = art.children().unwrap()[0].geometric_bounds().unwrap();
|
||||
// The disc runs from 20 to 80 pixels: half that, moved by (10, 20).
|
||||
assert!(near(b, [20.0, 30.0, 50.0, 60.0], 0.3), "{b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_off_center_outline_turns_and_reflects_with_a_smaller_copy_of_the_pixels() {
|
||||
// A 120×60 image whose opaque area is the rectangle from (12, 6) to (40, 24), its pixels a
|
||||
// copy at half size (as a linked image's preview is), turned a quarter and reflected.
|
||||
let half = image::DynamicImage::ImageRgba8(image::RgbaImage::from_fn(60, 30, |x, y| {
|
||||
image::Rgba([255, 0, 0, if (6..20).contains(&x) && (3..12).contains(&y) { 255 } else { 0 }])
|
||||
}));
|
||||
let (d, mut n) = image_doc(encoded(half, image::ImageFormat::Png), vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
let xf = Affine::translate((100.0, 40.0)) * Affine::rotate(std::f64::consts::FRAC_PI_2) * Affine::scale_non_uniform(-1.0, 1.0);
|
||||
if let NodeKind::Image(im) = &mut n.kind {
|
||||
(im.width, im.height, im.xf) = (120, 60, xf);
|
||||
}
|
||||
let rows = row_bounds(&d, &n);
|
||||
let want = xf.transform_rect_bbox(Rect::new(12.0, 6.0, 40.0, 24.0));
|
||||
// The rectangle runs from (76, 0) to (94, 28); a flip in either direction, or the copy's
|
||||
// width and height swapped, would move it.
|
||||
assert!(near(want, [76.0, 0.0, 94.0, 28.0], 1e-9), "{want:?}");
|
||||
assert!(rows.len() == 1 && near(rows[0], [want.x0, want.y0, want.x1, want.y1], 0.5), "{rows:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_image_in_a_group_exports_with_its_outline() {
|
||||
let (mut d, n) = image_doc(encoded(disc(), image::ImageFormat::Png), vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
let l = d.layers[0].id;
|
||||
d.remove(n.id).unwrap();
|
||||
let g = d.alloc_id();
|
||||
d.insert(Some(l), 0, Node::group(g, vec![Arc::new(n)])).unwrap();
|
||||
let baked = bake_document(&d).unwrap();
|
||||
let mut paths = vec![];
|
||||
for layer in &baked.layers {
|
||||
layer.walk(&mut |c| {
|
||||
if matches!(c.kind, NodeKind::Path { .. }) && (c.appearance.stroke_width() - 4.0).abs() < 1e-9 {
|
||||
paths.push(c.geometric_bounds().unwrap());
|
||||
}
|
||||
});
|
||||
}
|
||||
assert!(matches!(paths.as_slice(), [b] if near(*b, [70.0, 70.0, 130.0, 130.0], 0.5)), "{paths:?}");
|
||||
}
|
||||
|
||||
/// The bounds of the paths stroked 4 pt in `d` with its effects baked, as exports write it.
|
||||
fn baked_strokes(d: &Document) -> Vec<Rect> {
|
||||
let baked = bake_document(d).unwrap();
|
||||
let mut paths = vec![];
|
||||
for layer in &baked.layers {
|
||||
layer.walk(&mut |c| {
|
||||
if matches!(c.kind, NodeKind::Path { .. }) && (c.appearance.stroke_width() - 4.0).abs() < 1e-9 {
|
||||
paths.push(c.geometric_bounds().unwrap());
|
||||
}
|
||||
});
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_image_under_a_clipping_mask_or_with_an_opacity_mask_exports_with_its_outline() {
|
||||
let disc_outline = |paths: &[Rect]| matches!(paths, [b] if near(*b, [70.0, 70.0, 130.0, 130.0], 0.5));
|
||||
// Under a clipping mask that covers the whole image.
|
||||
let (mut d, n) = image_doc(encoded(disc(), image::ImageFormat::Png), vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
let l = d.layers[0].id;
|
||||
d.remove(n.id).unwrap();
|
||||
let clip = Node::path(d.alloc_id(), vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 200.0, 200.0)), Default::default());
|
||||
let g = d.alloc_id();
|
||||
d.insert(Some(l), 0, Node::new(g, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(n)], clip: true })).unwrap();
|
||||
let paths = baked_strokes(&d);
|
||||
assert!(disc_outline(&paths), "{paths:?}");
|
||||
// With an opacity mask.
|
||||
let (mut d, mut n) = image_doc(encoded(disc(), image::ImageFormat::Png), vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
let mut white = Node::path(NodeId(999), vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 200.0, 200.0)), Default::default());
|
||||
white.appearance.set_fill(Paint::solid(Color::WHITE));
|
||||
n.extras_mut().mask = Some(Box::new(vectorcraft_doc::OpacityMask::new(Node::group(NodeId(998), vec![Arc::new(white)]), true)));
|
||||
let id = n.id;
|
||||
*d.node_mut(id).unwrap() = n;
|
||||
let paths = baked_strokes(&d);
|
||||
assert!(disc_outline(&paths), "{paths:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_object_on_a_fill_and_on_an_indexed_image_follows_the_opaque_area() {
|
||||
let mut fill = FillLayer::new(Paint::solid(Color::rgb(0.0, 1.0, 0.0)));
|
||||
fill.effects = vec![fx("path.outlineObject")];
|
||||
let (d, n) = image_doc(encoded(disc(), image::ImageFormat::Png), vec![AppearanceItem::Fill(fill)], 1);
|
||||
let rows = row_bounds(&d, &n);
|
||||
assert!(matches!(rows.as_slice(), [b] if near(*b, [70.0, 70.0, 130.0, 130.0], 0.5)), "{rows:?}");
|
||||
// A palette image whose first color is transparent (a tRNS chunk).
|
||||
let mut rows = vec![];
|
||||
for y in 0..100 {
|
||||
rows.push(0);
|
||||
for x in 0..100 {
|
||||
let r = (f64::from(x) + 0.5 - 50.0).hypot(f64::from(y) + 0.5 - 50.0);
|
||||
rows.push(u8::from(r <= 30.0));
|
||||
}
|
||||
}
|
||||
let palette = png(100, 100, 3, 8, &[(b"PLTE", vec![0, 0, 0, 255, 0, 0]), (b"tRNS", vec![0, 255])], &rows);
|
||||
let (d, n) = image_doc(palette, vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
let rows = row_bounds(&d, &n);
|
||||
assert!(matches!(rows.as_slice(), [b] if near(*b, [70.0, 70.0, 130.0, 130.0], 0.5)), "{rows:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_image_too_large_or_too_complex_to_outline_keeps_its_frame() {
|
||||
// 72 megapixels at 8 bits a channel, and 42 at 16: refused before their pixels are read.
|
||||
for (w, h, depth) in [(9000, 8000, 8), (7000, 6000, 16)] {
|
||||
let bytes = png(w, h, 6, depth, &[], &[]);
|
||||
assert!(matches!(vectorcraft_trace::decode_alpha(&bytes), Err(vectorcraft_trace::TraceError::TooLarge { .. })), "{w}×{h}");
|
||||
let (d, n) = image_doc(bytes, vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
assert!(matches!(row_bounds(&d, &n).as_slice(), [b] if near(*b, [50.0, 50.0, 150.0, 150.0], 1e-9)), "{w}×{h}");
|
||||
}
|
||||
// A checkerboard of 2 px squares, whose outline would need more than 100,000 anchor points.
|
||||
let checker = image::RgbaImage::from_fn(640, 640, |x, y| image::Rgba([0, 0, 0, if (x / 2 + y / 2) % 2 == 0 { 255 } else { 0 }]));
|
||||
let (d, n) =
|
||||
image_doc(encoded(image::DynamicImage::ImageRgba8(checker), image::ImageFormat::Png), vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
assert!(matches!(row_bounds(&d, &n).as_slice(), [b] if near(*b, [50.0, 50.0, 150.0, 150.0], 1e-9)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_outline_is_traced_once_for_its_pixels_and_dropped_with_them() {
|
||||
let traces = || reshape::TRACES.with(std::cell::Cell::get);
|
||||
let (d, n) = image_doc(encoded(disc(), image::ImageFormat::Png), vec![stroke(vec![fx("path.outlineObject")])], 1);
|
||||
let NodeKind::Image(im) = &n.kind else { panic!("not an image") };
|
||||
let before = traces();
|
||||
let first = reshape::opaque_area(&[Some(&d)], im).unwrap();
|
||||
assert_eq!(traces(), before + 1);
|
||||
for _ in 0..3 {
|
||||
row_bounds(&d, &n);
|
||||
}
|
||||
assert_eq!(reshape::opaque_area(&[Some(&d)], im).unwrap(), first);
|
||||
assert_eq!(traces(), before + 1);
|
||||
let pixels = Arc::downgrade(&d.images.get(&im.key).unwrap().bytes);
|
||||
assert!(reshape::keeps_outline(&pixels));
|
||||
// Once the document and its pixels are gone, the next trace drops their entry.
|
||||
drop(d);
|
||||
assert!(pixels.upgrade().is_none());
|
||||
let (other, _) = image_doc(encoded(disc(), image::ImageFormat::Png), vec![], 1);
|
||||
reshape::opaque_area(&[Some(&other)], im).unwrap();
|
||||
assert_eq!(traces(), before + 2);
|
||||
assert!(!reshape::keeps_outline(&pixels));
|
||||
}
|
||||
@@ -82,7 +82,7 @@ pub mod rasterfx;
|
||||
mod recolor;
|
||||
pub mod recovery;
|
||||
pub mod scripts;
|
||||
mod select;
|
||||
pub(crate) mod select;
|
||||
pub(crate) mod shaper;
|
||||
pub mod shortcutset;
|
||||
pub(crate) mod slices;
|
||||
|
||||
@@ -73,6 +73,8 @@ pub struct RasterImage {
|
||||
pub height: u32,
|
||||
/// The resolution the file declares (see [`super::ppi::resolution`]).
|
||||
pub ppi: Option<(f64, f64)>,
|
||||
/// A DCS file's clipping path, in the image's pixel space ([`crate::cmd::place::dcs`]).
|
||||
pub clip: Option<vectorcraft_geom::PathData>,
|
||||
}
|
||||
|
||||
pub(super) fn err(e: impl std::fmt::Display) -> EngineError {
|
||||
@@ -380,6 +382,10 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
let (mut doc, mut warnings, restored) = match format.id {
|
||||
// EPS, .ps and PostScript .ai: the document our EPS files carry, else the PostScript read.
|
||||
_ if matches!(format.id, "eps" | "ps") || vectorcraft_pdf::is_postscript(bytes) => {
|
||||
// A DCS file's plates print from its file: it is only placed linked.
|
||||
if crate::cmd::place::dcs::is_dcs(bytes) {
|
||||
return Err(err(format!("can't open `{}`: {}", file_name(name), crate::cmd::place::dcs::LINK_ONLY)));
|
||||
}
|
||||
let editing = vectorcraft_eps::has_native(bytes).then_some((true, || vectorcraft_eps::native(bytes)));
|
||||
restore_or_import(editing, || {
|
||||
let r = vectorcraft_eps::import_names(bytes, opts.editing_data, opts.trust_editing_data, opts.legacy_names)
|
||||
@@ -1014,13 +1020,13 @@ fn stored(bytes: Vec<u8>, mime: &'static str, (width, height): (u32, u32), ppi:
|
||||
return Err(err("the image is empty"));
|
||||
}
|
||||
let blob = ImageBlob::new(mime, bytes);
|
||||
Ok(RasterImage { key: blob.content_key(), blob, width, height, ppi })
|
||||
Ok(RasterImage { key: blob.content_key(), blob, width, height, ppi, clip: None })
|
||||
}
|
||||
|
||||
/// An image as a document of its physical size at the resolution it declares (as Place sizes it;
|
||||
/// 72 ppi, 1 px = 1 pt, when it declares none), the image named after the file.
|
||||
pub(super) fn raster_doc(name: &str, bytes: &[u8]) -> Result<Document> {
|
||||
let RasterImage { key, blob, width, height, ppi } = raster_image(bytes)?;
|
||||
let RasterImage { key, blob, width, height, ppi, .. } = raster_image(bytes)?;
|
||||
let (sx, sy) = crate::cmd::place::pt_per_px(ppi);
|
||||
let mut d = Document::new(width as f64 * sx, height as f64 * sy);
|
||||
let layer = d.layers[0].id;
|
||||
|
||||
+446
-40
@@ -39,7 +39,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Select Text by Font",
|
||||
[],
|
||||
None,
|
||||
"{family, style?, scope?: \"document\" (default) | {artboards: [index, …] (0-based)} | {layers: [id, …] (layers or sublayers)}} select the text objects using the font, the instances of the symbols whose art uses it, the objects painted with a pattern whose art uses it and the objects whose opacity mask uses it (in type that is not hidden or locked in that art), that are not hidden or locked, in a hidden or locked group or layer, or on a locked artboard; a pattern swatch that nothing paints, or a layer's mask, selects nothing; with scope, only those on one of the artboards (as All on Active Artboard takes them: their bounds overlap it or their center is on it) or in one of the layers → {count}",
|
||||
"{family, style?, scope?: \"document\" (default) | {artboards: [index, …] (0-based)} | {layers: [id, …] (layers or sublayers)}} select the text objects using the font, the instances of the symbols whose art uses it, the text whose inline graphics draw such a symbol, the objects painted with a pattern whose art uses it (by a visible fill or stroke whose opacity is above 0, a stroke wider than 0) and the objects whose opacity mask uses it (in type that is not hidden or locked in that art), that are not hidden or locked, in a hidden or locked group or layer, or on a locked artboard; a pattern swatch that nothing paints, or a layer's mask, selects nothing; with scope, only those on one of the artboards (as All on Active Artboard takes them: their bounds overlap it or their center is on it) or in one of the layers → {count}",
|
||||
has_doc,
|
||||
select_font
|
||||
),
|
||||
@@ -108,23 +108,85 @@ fn pattern_being_edited(d: &Document, name: &str) -> bool {
|
||||
d.pattern_edit.as_ref().is_some_and(|e| e.pattern == name)
|
||||
}
|
||||
|
||||
/// Whether fill or stroke `i` paints: it is visible, its opacity is above 0 and, a stroke, it is
|
||||
/// wider than 0.
|
||||
fn paints(i: &AppearanceItem) -> bool {
|
||||
i.visible() && i.opacity() > 0.0 && !matches!(i, AppearanceItem::Stroke(s) if s.width <= 0.0)
|
||||
}
|
||||
|
||||
/// The name of pattern paint `p`.
|
||||
fn pattern_name(p: &Paint) -> Option<&str> {
|
||||
match p {
|
||||
Paint::Pattern { pattern, .. } => Some(pattern.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `r` is laid out as an inline graphic: it has one and holds only its character.
|
||||
fn draws_inline(r: &TextRun) -> bool {
|
||||
r.inline.is_some() && vectorcraft_text::edit::is_inline_text(&r.text)
|
||||
}
|
||||
|
||||
/// The fill and, when it is wider than 0, the stroke of the characters of `r`.
|
||||
fn run_paints(r: &TextRun) -> impl Iterator<Item = &Paint> {
|
||||
[Some(&r.style.fill), (r.style.stroke_width > 0.0).then_some(&r.style.stroke)].into_iter().flatten()
|
||||
}
|
||||
|
||||
/// [`run_paints`] of run `r` of `t` when its characters draw with them. The characters of an
|
||||
/// inline graphic draw no glyph: their fill and stroke paint only their underline and
|
||||
/// strikethrough (not on a path or in vertical type, see `vectorcraft_text::decorations`) and the
|
||||
/// ruby readings that layout draws with their run (`ruby`, as [`ruby_runs`] finds them).
|
||||
fn drawn_run_paints<'a>(t: &TextObject, ruby: &HashSet<*const TextRun>, r: &'a TextRun) -> impl Iterator<Item = &'a Paint> {
|
||||
let drawn = !draws_inline(r) || inline_decorated(t, ruby, r);
|
||||
run_paints(r).filter(move |_| drawn)
|
||||
}
|
||||
|
||||
/// The inline graphic runs of `t` that layout draws a ruby reading with. A reading takes the run
|
||||
/// of the first base character on its line, and type on a path has no ruby. `t` is laid out only
|
||||
/// when it has ruby and an inline graphic painted with a pattern.
|
||||
fn ruby_runs(t: &TextObject) -> HashSet<*const TextRun> {
|
||||
let patterned = |r: &TextRun| draws_inline(r) && run_paints(r).any(|p| pattern_name(p).is_some());
|
||||
if t.ruby.is_empty() || !t.runs.iter().any(patterned) {
|
||||
return HashSet::new();
|
||||
}
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
|
||||
lay.ruby.iter().filter_map(|g| t.runs.get(g.run)).filter(|r| draws_inline(r)).map(std::ptr::from_ref).collect()
|
||||
}
|
||||
|
||||
/// Whether inline graphic run `r` of `t` has an underline or strikethrough that draws, or a ruby
|
||||
/// reading that layout draws with it (it is in `ruby`).
|
||||
fn inline_decorated(t: &TextObject, ruby: &HashSet<*const TextRun>, r: &TextRun) -> bool {
|
||||
let bars = !t.vertical && !matches!(t.kind, TextKind::OnPath { .. });
|
||||
if bars && (r.style.underline || r.style.strikethrough) {
|
||||
return true;
|
||||
}
|
||||
ruby.contains(&std::ptr::from_ref(r))
|
||||
}
|
||||
|
||||
/// The patterns that the fills and strokes `n` draws name (of a text object, also those of its
|
||||
/// characters): visible ones, and strokes wider than 0.
|
||||
fn paint_patterns(n: &Node) -> impl Iterator<Item = &str> {
|
||||
let runs = match &n.kind {
|
||||
NodeKind::Text(t) => t.runs.as_slice(),
|
||||
_ => &[],
|
||||
};
|
||||
n.appearance
|
||||
.items
|
||||
.iter()
|
||||
.filter(|i| i.visible() && !matches!(i, AppearanceItem::Stroke(s) if s.width <= 0.0))
|
||||
.map(AppearanceItem::paint)
|
||||
.chain(runs.iter().flat_map(|r| [Some(&r.style.fill), (r.style.stroke_width > 0.0).then_some(&r.style.stroke)]).flatten())
|
||||
.filter_map(|p| match p {
|
||||
Paint::Pattern { pattern, .. } => Some(pattern.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
/// characters): those that [`paints`] holds for, and the character paints of the runs that draw
|
||||
/// (as [`drawn_runs`] finds them: not the overset runs of area type or type on a path) where they
|
||||
/// draw (as [`drawn_run_paints`] says).
|
||||
fn paint_patterns(n: &Node) -> Vec<&str> {
|
||||
let mut out: Vec<&str> = n.appearance.items.iter().filter(|i| paints(i)).filter_map(|i| pattern_name(i.paint())).collect();
|
||||
if let NodeKind::Text(t) = &n.kind
|
||||
&& t.runs.iter().flat_map(run_paints).any(|p| pattern_name(p).is_some())
|
||||
{
|
||||
let ruby = ruby_runs(t);
|
||||
out.extend(drawn_runs(t).0.into_iter().flat_map(|r| drawn_run_paints(t, &ruby, r)).filter_map(pattern_name));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The symbols that the inline graphics in the type of `n` draw (as [`drawn_runs`] finds them: not
|
||||
/// those in the overset runs of area type or type on a path).
|
||||
fn inline_symbols(n: &Node) -> Vec<&str> {
|
||||
match &n.kind {
|
||||
NodeKind::Text(t) if t.runs.iter().any(draws_inline) => {
|
||||
drawn_runs(t).0.into_iter().filter(|r| draws_inline(r)).filter_map(|r| r.inline.as_ref().map(|art| art.symbol.as_str())).collect()
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Symbols and patterns, by name: those whose art is in scope.
|
||||
@@ -135,13 +197,15 @@ struct Resources {
|
||||
}
|
||||
|
||||
impl Resources {
|
||||
/// Adds what `n` itself shows: the symbol of an instance, the patterns its paints name, and
|
||||
/// what the art of its opacity mask shows (not hidden or locked objects of that art).
|
||||
/// Adds what `n` itself shows: the symbol of an instance, the symbols of the inline graphics in
|
||||
/// its type, the patterns its paints name, and what the art of its opacity mask shows (not
|
||||
/// hidden or locked objects of that art).
|
||||
fn add_own(&mut self, n: &Node) {
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind {
|
||||
self.symbols.insert(symbol.clone());
|
||||
}
|
||||
self.patterns.extend(paint_patterns(n).map(str::to_string));
|
||||
self.symbols.extend(inline_symbols(n).into_iter().map(str::to_string));
|
||||
self.patterns.extend(paint_patterns(n).into_iter().map(str::to_string));
|
||||
if let Some(m) = n.extras().mask.as_deref() {
|
||||
self.add(&m.art);
|
||||
}
|
||||
@@ -486,17 +550,16 @@ impl Printed<'_> {
|
||||
NodeKind::Text(t) => drawn_runs(t),
|
||||
_ => (vec![], false),
|
||||
};
|
||||
let items: Vec<&AppearanceItem> =
|
||||
n.appearance.items.iter().filter(|i| i.visible() && !matches!(i, AppearanceItem::Stroke(s) if s.width <= 0.0)).collect();
|
||||
let patterns: Vec<&str> = items
|
||||
.iter()
|
||||
.map(|i| i.paint())
|
||||
.chain(runs.iter().flat_map(|r| [Some(&r.style.fill), (r.style.stroke_width > 0.0).then_some(&r.style.stroke)]).flatten())
|
||||
.filter_map(|p| match p {
|
||||
Paint::Pattern { pattern, .. } => Some(pattern.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let items: Vec<&AppearanceItem> = n.appearance.items.iter().filter(|i| paints(i)).collect();
|
||||
let ruby = match &n.kind {
|
||||
NodeKind::Text(t) => ruby_runs(t),
|
||||
_ => HashSet::new(),
|
||||
};
|
||||
let run_patterns = runs.iter().flat_map(|r| match &n.kind {
|
||||
NodeKind::Text(t) => Some(drawn_run_paints(t, &ruby, r)),
|
||||
_ => None,
|
||||
});
|
||||
let patterns: Vec<&str> = items.iter().map(|i| i.paint()).chain(run_patterns.flatten()).filter_map(pattern_name).collect();
|
||||
let brushed = vectorcraft_brush::has_brush(n);
|
||||
if (mask.is_some() || !patterns.is_empty() || brushed) && areas.is_none_or(|a| !open || self.meets(n, a)) {
|
||||
if let Some(m) = mask {
|
||||
@@ -527,7 +590,7 @@ impl Printed<'_> {
|
||||
for r in runs {
|
||||
self.fonts.extend(run_fonts(r));
|
||||
// Inline graphics draw their symbols' art.
|
||||
if let Some(art) = &r.inline {
|
||||
if let Some(art) = r.inline.as_ref().filter(|_| draws_inline(r)) {
|
||||
self.symbol(&art.symbol);
|
||||
}
|
||||
}
|
||||
@@ -910,14 +973,16 @@ fn replace(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
/// Whether `n` or a descendant that is not hidden or locked, nor in a hidden or locked group,
|
||||
/// shows type in a font `uses` matches: text set in it, an instance of a symbol in `found`, a paint
|
||||
/// naming a pattern in `found`, or such type in the art of its opacity mask.
|
||||
/// shows type in a font `uses` matches: text set in it, an instance of a symbol in `found`, an
|
||||
/// inline graphic of a symbol in `found`, a paint naming a pattern in `found`, or such type in the
|
||||
/// art of its opacity mask.
|
||||
fn reaches(n: &Node, uses: &impl Fn(&TextObject) -> bool, found: &Resources) -> bool {
|
||||
n.visible
|
||||
&& !n.locked
|
||||
&& (matches!(&n.kind, NodeKind::Text(t) if uses(t))
|
||||
|| matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if found.symbols.contains(symbol))
|
||||
|| paint_patterns(n).any(|p| found.patterns.contains(p))
|
||||
|| inline_symbols(n).into_iter().any(|sym| found.symbols.contains(sym))
|
||||
|| paint_patterns(n).into_iter().any(|p| found.patterns.contains(p))
|
||||
|| n.extras().mask.as_deref().is_some_and(|m| reaches(&m.art, uses, found))
|
||||
|| n.children().is_some_and(|ch| ch.iter().any(|c| reaches(c, uses, found))))
|
||||
}
|
||||
@@ -964,14 +1029,16 @@ fn select_font(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
found.patterns.extend(patterns);
|
||||
}
|
||||
// The objects the Selection tool can reach that show such type: the instances of those
|
||||
// symbols, the objects that those patterns paint, and the objects whose opacity mask holds it
|
||||
// (not layers, which the Selection tool doesn't select).
|
||||
// symbols, the text whose inline graphics draw them, the objects that those patterns paint,
|
||||
// and the objects whose opacity mask holds it (not layers, which the Selection tool doesn't
|
||||
// select).
|
||||
let masked: HashSet<NodeId> = masked.into_iter().collect();
|
||||
if !found.symbols.is_empty() || !found.patterns.is_empty() || !masked.is_empty() {
|
||||
let texts: HashSet<NodeId> = hits.iter().copied().collect();
|
||||
d.walk(&mut |n: &Node| {
|
||||
let shows = matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if found.symbols.contains(symbol))
|
||||
|| paint_patterns(n).any(|p| found.patterns.contains(p))
|
||||
|| inline_symbols(n).into_iter().any(|sym| found.symbols.contains(sym))
|
||||
|| paint_patterns(n).into_iter().any(|p| found.patterns.contains(p))
|
||||
|| (masked.contains(&n.id) && n.extras().mask.as_deref().is_some_and(|m| reaches(&m.art, &uses, &found)));
|
||||
if shows && !n.is_layer() && !texts.contains(&n.id) && editable(n.id) {
|
||||
hits.push(n.id);
|
||||
@@ -1069,6 +1136,37 @@ fn font_list(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives the inline graphics in the type of [`art_texts_of`] the current bounds of their symbols'
|
||||
/// art, and lays out again the type whose inline graphics changed. A symbol drawn inline in the
|
||||
/// art of another symbol takes one more round, so there is at most one round per symbol.
|
||||
fn resolve_art_inline(doc: &mut Document) {
|
||||
for _ in 0..=doc.symbols.len() {
|
||||
let names: BTreeSet<String> =
|
||||
art_texts_of(doc).iter().flat_map(|(_, t, _)| t.runs.iter().filter_map(|r| r.inline.as_ref().map(|a| a.symbol.clone()))).collect();
|
||||
let bounds: BTreeMap<String, Option<Rect>> = names
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let b = doc.symbol_art_bounds(&name);
|
||||
(name, b)
|
||||
})
|
||||
.collect();
|
||||
let stale = |t: &TextObject| t.runs.iter().any(|r| r.inline.as_ref().is_some_and(|a| bounds.get(&a.symbol).is_some_and(|b| *b != a.bounds)));
|
||||
let mut changed = false;
|
||||
edit_art_texts_of(doc, &stale, &mut |t| {
|
||||
for a in t.runs.iter_mut().filter_map(|r| r.inline.as_mut()) {
|
||||
if let Some(b) = bounds.get(&a.symbol) {
|
||||
a.bounds = *b;
|
||||
}
|
||||
}
|
||||
refresh_bounds(t);
|
||||
changed = true;
|
||||
});
|
||||
if !changed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rescan(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -1076,7 +1174,9 @@ pub(crate) fn rescan(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let faces = 0;
|
||||
// The canvas redraws type in the fonts available now (renderers lay it out again when the
|
||||
// fonts change), and the bounds follow; that isn't an edit to save.
|
||||
// fonts change), and the bounds follow, also of the type in opacity masks and in the art of
|
||||
// symbols and patterns; that isn't an edit to save. The type in art is laid out first, and the
|
||||
// inline graphics of the type on the layers then take the new bounds of their symbols' art.
|
||||
for d in &mut s.docs {
|
||||
let mut texts = vec![];
|
||||
d.doc.walk(|n| {
|
||||
@@ -1084,9 +1184,12 @@ pub(crate) fn rescan(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
texts.push(n.id);
|
||||
}
|
||||
});
|
||||
if !texts.is_empty() {
|
||||
if !texts.is_empty() || !art_texts_of(&d.doc).is_empty() {
|
||||
d.change_unsaved(|doc| {
|
||||
let doc = std::sync::Arc::make_mut(doc);
|
||||
edit_art_texts_of(doc, &|_| true, &mut refresh_bounds);
|
||||
resolve_art_inline(doc);
|
||||
doc.resolve_inline_art();
|
||||
for id in texts {
|
||||
if let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) {
|
||||
refresh_bounds(t);
|
||||
@@ -1751,6 +1854,309 @@ mod tests {
|
||||
assert_eq!(symbol_texts(&s.doc().unwrap().doc, None)[0].0.runs[0].style.font_family, "Source Sans 3");
|
||||
}
|
||||
|
||||
/// A pattern in a fill whose opacity is 0 paints nothing: Find doesn't select the object for
|
||||
/// the pattern's type, a selection-only Change leaves that type in its font, and Print doesn't
|
||||
/// count its font.
|
||||
#[test]
|
||||
fn find_font_skips_patterns_at_an_opacity_of_0() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let t = s.execute("text.create", &json!({"x": 10, "y": 20, "text": "Ab", "font": "Source Serif 4"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.pattern.make", &json!({"ids": [t], "name": "Dots", "edit": false})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [t]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
let painted = s.execute("shape.rectangle", &json!({"x": 0, "y": 20, "width": 50, "height": 20})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("paint.setFill", &json!({"swatch": "Dots", "ids": [painted]})).unwrap();
|
||||
let printed = |s: &Session| {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
printed_type(d, &d.layers, &[Rect::new(-1000.0, -1000.0, 1000.0, 1000.0)]).0.into_iter().map(|(family, ..)| family).collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(s.execute("select.font", &json!({"family": "Source Serif 4"})).unwrap()["count"], 1);
|
||||
assert_eq!(printed(&s), ["Source Serif 4"]);
|
||||
s.edit("Clear the fill", |d, _| {
|
||||
for it in d.node_mut(NodeId(painted)).unwrap().appearance.items.iter_mut() {
|
||||
if let AppearanceItem::Fill(f) = it {
|
||||
f.opacity = 0.0;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(s.execute("select.font", &json!({"family": "Source Serif 4"})).unwrap()["count"], 0);
|
||||
assert!(printed(&s).is_empty(), "{:?}", printed(&s));
|
||||
s.execute("select.set", &json!({"ids": [painted]})).unwrap();
|
||||
let r = s
|
||||
.execute("text.replaceFont", &json!({"from": {"family": "Source Serif 4"}, "to": {"family": "Source Sans 3"}, "selectionOnly": true}))
|
||||
.unwrap();
|
||||
assert_eq!(r, json!({"runs": 0}));
|
||||
assert_eq!(pattern_families(&s, "Dots"), ["Source Serif 4"]);
|
||||
}
|
||||
|
||||
/// Characters of area type that don't fit its frame paint nothing: their pattern's type isn't
|
||||
/// found through them until they are drawn.
|
||||
#[test]
|
||||
fn find_font_skips_patterns_of_overset_characters() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let t = s.execute("text.create", &json!({"x": 10, "y": 20, "text": "Ab", "font": "Source Serif 4"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.pattern.make", &json!({"ids": [t], "name": "Dots", "edit": false})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [t]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
let area = s
|
||||
.execute(
|
||||
"text.create",
|
||||
&json!({"x": 10, "y": 60, "text": "A\nB", "font": "Source Sans 3", "size": 20, "area": {"width": 100, "height": 40}}),
|
||||
)
|
||||
.unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
s.execute("text.setRangeStyle", &json!({"id": area, "start": 2, "end": 3, "font": "Inter"})).unwrap();
|
||||
let paint_run = |s: &mut Session, letter: &'static str| {
|
||||
s.edit("Paint a run with the pattern", |d, _| {
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(NodeId(area)).map(|n| &mut n.kind) {
|
||||
for r in t.runs.iter_mut().filter(|r| r.text.contains(letter)) {
|
||||
r.style.fill = Paint::Pattern { pattern: "Dots".into(), xf: Default::default() };
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
};
|
||||
paint_run(&mut s, "B");
|
||||
let laid = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), text(&s.doc().unwrap().doc, NodeId(area)).unwrap());
|
||||
assert!(laid.overflow, "the second line doesn't fit the frame");
|
||||
assert_eq!(s.execute("select.font", &json!({"family": "Source Serif 4"})).unwrap()["count"], 0);
|
||||
paint_run(&mut s, "A");
|
||||
assert_eq!(s.execute("select.font", &json!({"family": "Source Serif 4"})).unwrap()["count"], 1);
|
||||
}
|
||||
|
||||
/// An inline graphic that doesn't fit its area type draws nothing, and neither does an inline
|
||||
/// graphic named on a run of other characters: Find doesn't select the text for its symbol.
|
||||
#[test]
|
||||
fn find_font_skips_undrawn_inline_graphics() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let t = s.execute("text.create", &json!({"x": 10, "y": 20, "text": "Ab", "font": "Source Serif 4"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.pattern.make", &json!({"ids": [t], "name": "Dots", "edit": false})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [t]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
let mark = s.execute("text.create", &json!({"x": 10, "y": 20, "text": "Se", "font": "Inter"})).unwrap()["id"].clone();
|
||||
s.execute("symbol.new", &json!({"name": "Mark", "ids": [mark]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
let area = s
|
||||
.execute(
|
||||
"text.create",
|
||||
&json!({"x": 10, "y": 60, "text": "A\nB", "font": "Source Sans 3", "size": 20, "area": {"width": 100, "height": 40}}),
|
||||
)
|
||||
.unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
s.execute("text.insertInline", &json!({"id": area, "at": 3, "symbol": "Mark"})).unwrap();
|
||||
let laid = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), text(&s.doc().unwrap().doc, NodeId(area)).unwrap());
|
||||
assert!(laid.overflow && laid.inlines.is_empty(), "the inline graphic doesn't fit the frame");
|
||||
assert_eq!(s.execute("select.font", &json!({"family": "Inter"})).unwrap()["count"], 0);
|
||||
let point = s.execute("text.create", &json!({"x": 10, "y": 140, "text": "Hi", "font": "Source Sans 3"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.insertInline", &json!({"id": point, "at": 0, "symbol": "Mark"})).unwrap();
|
||||
assert_eq!(s.execute("select.font", &json!({"family": "Inter"})).unwrap()["count"], 1);
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [NodeId(point)]);
|
||||
// A run that names an inline graphic but holds other characters is laid out as those
|
||||
// characters: its fill paints them, and it draws no symbol.
|
||||
let plain = s.execute("text.create", &json!({"x": 10, "y": 180, "text": "Ok", "font": "Source Sans 3"})).unwrap()["id"].as_u64().unwrap();
|
||||
let art = text(&s.doc().unwrap().doc, NodeId(point)).unwrap().runs.iter().find_map(|r| r.inline.clone()).unwrap();
|
||||
s.edit("Name an inline graphic on plain characters", |d, _| {
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(NodeId(plain)).map(|n| &mut n.kind) {
|
||||
for r in &mut t.runs {
|
||||
r.inline = Some(art.clone());
|
||||
r.style.fill = Paint::Pattern { pattern: "Dots".into(), xf: Default::default() };
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(s.execute("select.font", &json!({"family": "Source Serif 4"})).unwrap()["count"], 1);
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [NodeId(plain)]);
|
||||
assert_eq!(s.execute("select.font", &json!({"family": "Inter"})).unwrap()["count"], 1);
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [NodeId(point)]);
|
||||
}
|
||||
|
||||
/// The characters of an inline graphic draw no glyph: their pattern fill paints only their
|
||||
/// underline, strikethrough or ruby reading. Without one, Find doesn't select the text for the
|
||||
/// pattern's type and Print doesn't count its font.
|
||||
#[test]
|
||||
fn find_font_skips_patterns_of_undecorated_inline_graphics() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let t = s.execute("text.create", &json!({"x": 10, "y": 20, "text": "Ab", "font": "Source Serif 4"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.pattern.make", &json!({"ids": [t], "name": "Dots", "edit": false})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [t]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
let mark = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].clone();
|
||||
s.execute("symbol.new", &json!({"name": "Mark", "ids": [mark]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
let point = s.execute("text.create", &json!({"x": 10, "y": 100, "text": "Hi", "font": "Source Sans 3"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.insertInline", &json!({"id": point, "at": 1, "symbol": "Mark"})).unwrap();
|
||||
let style = |s: &mut Session, f: &dyn Fn(&mut TextObject)| {
|
||||
s.edit("Style the inline graphic", |d, _| {
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(NodeId(point)).map(|n| &mut n.kind) {
|
||||
f(t);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
};
|
||||
let found = |s: &mut Session| s.execute("select.font", &json!({"family": "Source Serif 4"})).unwrap()["count"].as_u64();
|
||||
let printed = |s: &Session| {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
printed_type(d, &d.layers, &[Rect::new(-1000.0, -1000.0, 1000.0, 1000.0)]).0.into_iter().map(|(family, ..)| family).collect::<Vec<_>>()
|
||||
};
|
||||
style(&mut s, &|t| {
|
||||
for r in t.runs.iter_mut().filter(|r| r.inline.is_some()) {
|
||||
r.style.fill = Paint::Pattern { pattern: "Dots".into(), xf: Default::default() };
|
||||
}
|
||||
});
|
||||
assert_eq!(found(&mut s), Some(0));
|
||||
assert_eq!(printed(&s), ["Source Sans 3"]);
|
||||
// An underline paints with the pattern.
|
||||
style(&mut s, &|t| t.runs.iter_mut().filter(|r| r.inline.is_some()).for_each(|r| r.style.underline = true));
|
||||
assert_eq!(found(&mut s), Some(1));
|
||||
assert_eq!(printed(&s), ["Source Sans 3", "Source Serif 4"]);
|
||||
// Vertical type draws no underline.
|
||||
style(&mut s, &|t| t.vertical = true);
|
||||
assert_eq!(found(&mut s), Some(0));
|
||||
// A ruby reading that starts over the character before the inline graphic draws with that
|
||||
// character's paint.
|
||||
style(&mut s, &|t| {
|
||||
assert!(t.runs[1].inline.is_some(), "the inline graphic is the second run");
|
||||
t.ruby.push(vectorcraft_doc::RubySpan {
|
||||
start: 0,
|
||||
end: t.runs[0].text.len() + t.runs[1].text.len(),
|
||||
text: "x".into(),
|
||||
position: Default::default(),
|
||||
align: Default::default(),
|
||||
size_percent: 50.0,
|
||||
overhang: true,
|
||||
});
|
||||
});
|
||||
assert_eq!(found(&mut s), Some(0));
|
||||
// A ruby reading that starts over the inline graphic paints with the pattern.
|
||||
style(&mut s, &|t| t.ruby[0].start = t.runs[0].text.len());
|
||||
assert_eq!(found(&mut s), Some(1));
|
||||
}
|
||||
|
||||
/// Text whose inline graphic draws a symbol shows the symbol's type: Find selects that text
|
||||
/// and the objects painted with a pattern whose art holds it, and a selection-only Change
|
||||
/// changes the symbol's type.
|
||||
#[test]
|
||||
fn find_font_reaches_symbols_through_inline_graphics() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let serif = s.execute("text.create", &json!({"x": 10, "y": 20, "text": "Se", "font": "Source Serif 4"})).unwrap()["id"].clone();
|
||||
s.execute("symbol.new", &json!({"name": "Mark", "ids": [serif]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
let inline = |s: &mut Session, y: i32| {
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": y, "text": "In", "font": "Inter"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.insertInline", &json!({"id": id, "at": 0, "symbol": "Mark"})).unwrap();
|
||||
id
|
||||
};
|
||||
let listed = inline(&mut s, 60);
|
||||
let tiled = inline(&mut s, 100);
|
||||
s.execute("object.pattern.make", &json!({"ids": [tiled], "name": "Loop", "edit": false})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [tiled]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
let painted = s.execute("shape.rectangle", &json!({"x": 0, "y": 140, "width": 50, "height": 20})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("paint.setFill", &json!({"swatch": "Loop", "ids": [painted]})).unwrap();
|
||||
let mut instances = 0;
|
||||
s.doc().unwrap().doc.walk(|n| instances += usize::from(matches!(n.kind, NodeKind::SymbolInstance { .. })));
|
||||
assert_eq!(instances, 0, "the symbol is drawn only by inline graphics");
|
||||
assert_eq!(s.execute("select.font", &json!({"family": "Source Serif 4"})).unwrap()["count"], 2);
|
||||
let mut selected = s.doc().unwrap().selection.objects.clone();
|
||||
selected.sort();
|
||||
assert_eq!(selected, [NodeId(listed), NodeId(painted)]);
|
||||
s.execute("select.set", &json!({"ids": [painted]})).unwrap();
|
||||
let rows = font_rows(&mut s, json!({"selectionOnly": true}));
|
||||
assert_eq!(rows, [("Inter".to_string(), 2, 1), ("Source Serif 4".to_string(), 1, 1)]);
|
||||
let r = s
|
||||
.execute("text.replaceFont", &json!({"from": {"family": "Source Serif 4"}, "to": {"family": "Source Sans 3"}, "selectionOnly": true}))
|
||||
.unwrap();
|
||||
assert_eq!(r, json!({"runs": 1}));
|
||||
assert_eq!(symbol_texts(&s.doc().unwrap().doc, None)[0].0.runs[0].style.font_family, "Source Sans 3");
|
||||
}
|
||||
|
||||
/// Refresh Font List lays out again the type in opacity masks and in the art of symbols and
|
||||
/// patterns, as it does the type on the layers, and the inline graphics of the type on the
|
||||
/// layers follow the symbols' type as laid out again.
|
||||
#[test]
|
||||
fn rescanning_fonts_refreshes_the_bounds_of_type_in_art() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let owner = s.execute("shape.rectangle", &json!({"x": 0, "y": 20, "width": 50, "height": 20})).unwrap()["id"].as_u64().unwrap();
|
||||
let masked = s.execute("text.create", &json!({"x": 10, "y": 20, "text": "Mask", "font": "Inter"})).unwrap()["id"].as_u64().unwrap();
|
||||
mask_with(&mut s, owner, masked);
|
||||
let tiled = s.execute("text.create", &json!({"x": 10, "y": 60, "text": "Tile", "font": "Inter"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.pattern.make", &json!({"ids": [tiled], "name": "Dots", "edit": false})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [tiled]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
let stale = Rect::new(0.0, 0.0, 1.0, 1.0);
|
||||
s.edit("Make the bounds stale", |d, _| {
|
||||
edit_art_texts_of(d, &|_| true, &mut |t| t.cached_bounds = Some(stale));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let bounds = |s: &Session| art_texts_of(&s.doc().unwrap().doc).iter().map(|(_, t, _)| t.cached_bounds).collect::<Vec<_>>();
|
||||
assert_eq!(bounds(&s), [Some(stale), Some(stale)]);
|
||||
s.execute("text.rescanFonts", &json!({})).unwrap();
|
||||
let after = bounds(&s);
|
||||
assert_eq!(after.len(), 2);
|
||||
assert!(after.iter().all(|b| b.is_some_and(|b| b != stale && b.width() > 1.0)), "{after:?}");
|
||||
let marked = s.execute("text.create", &json!({"x": 10, "y": 100, "text": "Se", "font": "Inter"})).unwrap()["id"].clone();
|
||||
s.execute("symbol.new", &json!({"name": "Mark", "ids": [marked]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
let line = s.execute("text.create", &json!({"x": 10, "y": 140, "text": "In", "font": "Inter"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.insertInline", &json!({"id": line, "at": 0, "symbol": "Mark"})).unwrap();
|
||||
s.edit("Make the bounds stale", |d, _| {
|
||||
edit_art_texts_of(d, &|_| true, &mut |t| t.cached_bounds = Some(stale));
|
||||
d.resolve_inline_art();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let resolved = |s: &Session| {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
matches!(d.inline_resolved(text(d, NodeId(line)).unwrap()), std::borrow::Cow::Borrowed(_))
|
||||
};
|
||||
assert!(resolved(&s), "the inline graphic took the stale bounds");
|
||||
s.execute("text.rescanFonts", &json!({})).unwrap();
|
||||
assert!(resolved(&s), "the inline graphic follows the symbol's type laid out again");
|
||||
// So does an inline graphic in the art of another symbol.
|
||||
let held = s.execute("text.create", &json!({"x": 10, "y": 180, "text": "Out", "font": "Inter"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.insertInline", &json!({"id": held, "at": 0, "symbol": "Mark"})).unwrap();
|
||||
s.execute("symbol.new", &json!({"name": "Outer", "ids": [held]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
s.edit("Make the bounds stale", |d, _| {
|
||||
edit_art_texts_of(d, &|_| true, &mut |t| t.cached_bounds = Some(stale));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let shrunk = s.doc().unwrap().doc.symbol_art_bounds("Mark");
|
||||
s.edit("Take the stale bounds", |d, _| {
|
||||
edit_art_texts_of(d, &|_| true, &mut |t| {
|
||||
for a in t.runs.iter_mut().filter_map(|r| r.inline.as_mut()) {
|
||||
a.bounds = shrunk;
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let nested = |s: &Session| {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
art_texts_of(d).iter().flat_map(|(_, t, _)| t.runs.iter().filter_map(|r| r.inline.as_ref().map(|a| a.bounds))).collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(nested(&s), [shrunk]);
|
||||
s.execute("text.rescanFonts", &json!({})).unwrap();
|
||||
let mark = s.doc().unwrap().doc.symbol_art_bounds("Mark");
|
||||
assert_ne!(mark, shrunk);
|
||||
assert_eq!(nested(&s), [mark], "the inline graphic in the art of Outer follows Mark's type laid out again");
|
||||
}
|
||||
|
||||
/// The font family of text object `id`'s first run.
|
||||
fn text_family(s: &Session, id: u64) -> String {
|
||||
text(&s.doc().unwrap().doc, NodeId(id)).unwrap().runs[0].style.font_family.clone()
|
||||
|
||||
+312
-54
@@ -31,10 +31,10 @@ use vectorcraft_doc::links::{Align, Preserve, hash_bytes};
|
||||
use vectorcraft_doc::{
|
||||
Appearance, DataValue, Document, ImageBlob, ImageObject, LinkInfo, Node, NodeId, NodeKind, PageBox, PlacedDocument, PlacementOptions,
|
||||
};
|
||||
use vectorcraft_geom::{Affine, PathData, Rect, shapes};
|
||||
use vectorcraft_geom::{Affine, FillRule, PathData, Rect, shapes};
|
||||
|
||||
use super::fileio::{self, RasterImage, absolute_path, file_created, file_stamp, read_file, write_file};
|
||||
use super::{CommandSpec, EngineError, Result, Session, bad, bool_or, cmd, has_doc, ids_param, object_param, str_param};
|
||||
use super::{CommandSpec, EngineError, Result, Session, bad, bool_or, cmd, has_doc, ids_param, object_param, str_param, switch_or};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
@@ -52,7 +52,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Update Links",
|
||||
[],
|
||||
None,
|
||||
"{ids?: [image or placed document ids] (default: the linked images whose file was modified or is showing its preview, and placed documents whose file was modified)} read their linked files again; each keeps its bounds. The images and placed documents inside symbols, graph designs, patterns and opacity masks that show the same content are read again too (by default also those only that art shows); they have no ids to list. One undo step → {updated: [ids], missing: [ids]}",
|
||||
"{ids?: [image or placed document ids] (default: the linked images whose file was modified or is showing its preview, and placed documents whose file was modified)} read their linked files again; each keeps its bounds. The images and placed documents inside symbols, graph designs, patterns and opacity masks that show the same content are read again too (by default also those only that art shows); they have no ids to list. One undo step → {updated: [ids], missing: [ids], warnings?: [a DCS file's spot plate whose ink a swatch of the document already names, as file.place warns]}",
|
||||
has_doc,
|
||||
update
|
||||
),
|
||||
@@ -70,7 +70,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Relink",
|
||||
[],
|
||||
None,
|
||||
"{ids?: [image or placed document ids] (default: the selected ones; with folder and nothing selected, every missing link), path | folder, scope?: selected (default: ids) | all (ids and every other object in the layers linked to the same file as one of them) | missingInFolder (with path: ids, then every other missing link whose file name is in path's folder, each to the file of its name there; names not there are in notFound; with copy, only images are looked for) | each (with paths), paths?: [{ids, path}] (with scope each, in place of ids, path and folder: each link to its own file; an id in two entries is an error), copy?: false (with path or paths, for images of a saved document: copy the file into the document's folder and link to the copy; the names Name.png, Name 2.png … Name 99.png are tried in turn until one is free, which gets the copy, or is a file with the same contents, which is used), preserve?: transforms|bounds|fileDimensions|fit|fill (how the new file takes each image's place this time, as links.placementOptions describes; default: each image's Placement Options; fit keeps the new file's aspect ratio inside the old bounds: Replace)} link images (or placed documents, each showing the same artboard or page of the new file) to the file at path, or each to the file of its link's name in folder; embedded images become linked; each keeps its bounds. One undo step → {relinked: [ids], notFound: [file names], copied?: [{from, to}], kept?: [{from, to}] (with copy; to is the file name alone when the document's folder is outside the automation read roots)}",
|
||||
"{ids?: [image or placed document ids] (default: the selected ones; with folder and nothing selected, every missing link), path | folder, scope?: selected (default: ids) | all (ids and every other object in the layers linked to the same file as one of them) | missingInFolder (with path: ids, then every other missing link whose file name is in path's folder, each to the file of its name there; names not there are in notFound; with copy, only images are looked for) | each (with paths), paths?: [{ids, path}] (with scope each, in place of ids, path and folder: each link to its own file; an id in two entries is an error), copy?: false (with path or paths, for images of a saved document: copy the file into the document's folder and link to the copy; the names Name.png, Name 2.png … Name 99.png are tried in turn until one is free, which gets the copy, or is a file with the same contents, which is used; an error for a DCS file that reads plate files of its own), preserve?: transforms|bounds|fileDimensions|fit|fill (how the new file takes each image's place this time, as links.placementOptions describes; default: each image's Placement Options; fit keeps the new file's aspect ratio inside the old bounds: Replace)} link images (or placed documents, each showing the same artboard or page of the new file) to the file at path, or each to the file of its link's name in folder; embedded images become linked; each keeps its bounds. One undo step → {relinked: [ids], notFound: [file names], copied?: [{from, to}], kept?: [{from, to}] (with copy; to is the file name alone when the document's folder is outside the automation read roots), warnings?: (as links.update's)}",
|
||||
has_doc,
|
||||
relink
|
||||
),
|
||||
@@ -79,7 +79,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Links",
|
||||
[],
|
||||
None,
|
||||
"{show?: all (default)|missing|modified|embedded, sort?: name|kind (file format)|status (missing, modified, ok, embedded); default: stacking order; top first} every image object and placed document in the layers, as the Links panel lists them → {links: [{id, name, linked, status: ok|modified|missing|embedded, format, pixelWidth, pixelHeight, path?, found?: path (found away from its path), page?, preview?: true (showing the saved preview), note?: said of an image past the decode allowance (shown at reduced size, or too large to draw), document?: true (a placed document: pageWidth and pageHeight in pt instead of pixels)}], missing, modified, embedded}; under automation read roots, path and found are left out when the file they name is outside the roots",
|
||||
"{show?: all (default)|missing|modified|embedded, sort?: name|kind (file format)|status (missing, modified, ok, embedded); default: stacking order; top first, dcsTransparency?: false (Show DCS Transparency Interactions: mark the images placed from DCS files that are transparent, themselves or a group or layer holding them, or that overlap a visible object with its own transparency; it looks at every object, so it takes longer)} every image object and placed document in the layers, as the Links panel lists them → {links: [{id, name, linked, status: ok|modified|missing|embedded, format (DCS for an image placed from a DCS file), pixelWidth, pixelHeight, path?, found?: path (found away from its path), page?, preview?: true (showing the saved preview), note?: said of an image past the decode allowance (shown at reduced size, or too large to draw), document?: true (a placed document: pageWidth and pageHeight in pt instead of pixels), dcsTransparency?: true (with dcsTransparency)}], missing, modified, embedded}; under automation read roots, path and found are left out when the file they name is outside the roots",
|
||||
has_doc,
|
||||
list
|
||||
),
|
||||
@@ -97,7 +97,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Embed Image",
|
||||
[],
|
||||
None,
|
||||
"{ids?: [image or placed document ids] (default: the selected linked ones)} break the links (the Links panel's Break Link): images keep the linked files' pixels in the document; a placed document becomes an editable copy of its art, as placing its file without link gives (its symbols, patterns, swatches and images joining the document). An image whose file can't be found and that shows the saved preview, or a placed document whose file can't be read or changed, stays linked: relink or update it first. One undo step → {embedded: [ids], missing: [ids]}",
|
||||
"{ids?: [image or placed document ids] (default: the selected linked ones)} break the links (the Links panel's Break Link): images keep the linked files' pixels in the document; a placed document becomes an editable copy of its art, as placing its file without link gives (its symbols, patterns, swatches and images joining the document). An image whose file can't be found and that shows the saved preview, or a placed document whose file can't be read or changed, stays linked: relink or update it first. An image placed from a DCS file can't be embedded (an error, and nothing changes). One undo step → {embedded: [ids], missing: [ids]}",
|
||||
has_doc,
|
||||
embed
|
||||
),
|
||||
@@ -160,6 +160,8 @@ struct Group {
|
||||
ids: Vec<NodeId>,
|
||||
/// Placed documents: what they show.
|
||||
document: Option<Shown>,
|
||||
/// An image placed from a DCS file: the plate files of their own its plates were read from.
|
||||
plate_files: Vec<vectorcraft_doc::separated::PlateFile>,
|
||||
}
|
||||
|
||||
/// What a placed document shows: whether the art's bounds (not the artboard or page) are shown,
|
||||
@@ -175,9 +177,17 @@ fn linked(n: &Node) -> Option<(&LinkInfo, &str, Option<Shown>)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Every linked file of `d` with the ids of the objects showing it (for [`crate::link_watch`]).
|
||||
/// Every linked file of `d` with the ids of the objects showing it (for [`crate::link_watch`]): an
|
||||
/// image placed from a DCS file has its plate files of their own too, beside its file.
|
||||
pub(crate) fn linked_groups(d: &Document) -> Vec<(String, Vec<u64>)> {
|
||||
groups(d, None).into_iter().filter(|g| !g.ids.is_empty()).map(|g| (g.link.path, ids_json(&g.ids))).collect()
|
||||
let mut out = vec![];
|
||||
for g in groups(d, None).into_iter().filter(|g| !g.ids.is_empty()) {
|
||||
let ids = ids_json(&g.ids);
|
||||
let plates: Vec<String> = g.plate_files.iter().filter_map(|f| super::place::dcs::plate_file_path(&g.link.path, &f.name)).collect();
|
||||
out.push((g.link.path, ids.clone()));
|
||||
out.extend(plates.into_iter().map(|p| (p, ids.clone())));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The linked images and placed documents of `d` by file and content: those in the layers (only `ids` when
|
||||
@@ -189,7 +199,11 @@ fn groups(d: &Document, ids: Option<&[NodeId]>) -> Vec<Group> {
|
||||
let wanted: Option<std::collections::HashSet<NodeId>> = ids.map(|ids| ids.iter().copied().collect());
|
||||
let mut add = |link: &LinkInfo, key: &str, document: Option<Shown>, id: Option<NodeId>| {
|
||||
let i = *index.entry((link.path.clone(), key.to_string(), document.is_some())).or_insert_with(|| {
|
||||
out.push(Group { link: link.clone(), key: key.to_string(), ids: vec![], document });
|
||||
let plate_files = match document {
|
||||
None => d.images.get(key).map(ImageBlob::plate_files).unwrap_or_default(),
|
||||
Some(_) => vec![],
|
||||
};
|
||||
out.push(Group { link: link.clone(), key: key.to_string(), ids: vec![], document, plate_files });
|
||||
out.len() - 1
|
||||
});
|
||||
if let (Some(id), Some(g)) = (id, out.get_mut(i)) {
|
||||
@@ -335,10 +349,14 @@ fn refused(link: &LinkInfo, doc_path: Option<&str>) -> Option<craft_docpath::Ref
|
||||
}
|
||||
|
||||
/// Where the file of group `g` (of a document looking from `base`) is and whether it still has
|
||||
/// the content shown. Its bytes are read only when its size or modification time changed.
|
||||
/// the content shown. Its bytes are read only when its size or modification time changed. An image
|
||||
/// placed from a DCS file is modified when one of its plate files of their own changed.
|
||||
fn probe(g: &Group, base: LinkBase) -> (Status, Option<Found>) {
|
||||
let Some(mut f) = locate(&g.link, base) else { return (Status::Missing, None) };
|
||||
let l = &g.link;
|
||||
if !g.plate_files.is_empty() && super::place::dcs::plate_files_changed(&g.plate_files, &f.path) {
|
||||
return (Status::Modified, Some(f));
|
||||
}
|
||||
if l.size == Some(f.size) && l.modified.is_some() && l.modified == f.modified {
|
||||
return (Status::Ok, Some(f));
|
||||
}
|
||||
@@ -346,7 +364,7 @@ fn probe(g: &Group, base: LinkBase) -> (Status, Option<Found>) {
|
||||
let same = match &l.hash {
|
||||
Some(h) => *h == hash_bytes(&bytes),
|
||||
// Linked before links kept a hash: the image the file decodes to.
|
||||
None => g.document.is_none() && fileio::raster_image(&bytes).is_ok_and(|img| img.key == g.key),
|
||||
None => g.document.is_none() && super::place::dcs::raster(&bytes, Some(&f.path)).is_ok_and(|img| img.key == g.key),
|
||||
};
|
||||
f.bytes = Some(bytes);
|
||||
(if same { Status::Ok } else { Status::Modified }, Some(f))
|
||||
@@ -433,7 +451,7 @@ pub(crate) fn full_linked_images<'a>(d: &'a Document, base: LinkBase) -> (Cow<'a
|
||||
previews.insert(key);
|
||||
continue;
|
||||
};
|
||||
let Ok(img) = fileio::raster_image(&bytes) else {
|
||||
let Ok(img) = super::place::dcs::raster(&bytes, Some(&f.path)) else {
|
||||
previews.insert(key);
|
||||
continue;
|
||||
};
|
||||
@@ -512,10 +530,11 @@ pub(crate) fn readable_and_kept(blob: ImageBlob, unread: bool, linked: bool) ->
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
/// The linked image file `bytes`, read ([`fileio::raster_image`]) with its preview; an error when
|
||||
/// its pixels can't be read ([`readable`]).
|
||||
fn linked_image(bytes: &[u8]) -> Result<RasterImage> {
|
||||
let mut img = fileio::raster_image(bytes)?;
|
||||
/// The linked image file `bytes` (read from `path`, when there is one), read with its preview: a
|
||||
/// DCS file's plates, else its image ([`super::place::dcs::raster`]); an error when its pixels
|
||||
/// can't be read ([`readable`]).
|
||||
fn linked_image(bytes: &[u8], path: Option<&str>) -> Result<RasterImage> {
|
||||
let mut img = super::place::dcs::raster(bytes, path)?;
|
||||
img.blob = readable(img.blob, kept_unread(bytes), true)?;
|
||||
Ok(img)
|
||||
}
|
||||
@@ -536,7 +555,7 @@ fn read_image(f: &mut Found, base: &LinkInfo, reads: &mut ImageReads) -> Result<
|
||||
let img = match reads.get(&f.path) {
|
||||
Some((h, img)) if *h == hash => img.clone(),
|
||||
_ => {
|
||||
let img = linked_image(&bytes)?;
|
||||
let img = linked_image(&bytes, Some(&f.path))?;
|
||||
reads.insert(f.path.clone(), (hash.clone(), img.clone()));
|
||||
img
|
||||
}
|
||||
@@ -586,12 +605,89 @@ fn clip_shape((frame, room): Clip) -> PathData {
|
||||
shapes::rectangle(Rect::new(0.0, 0.0, room.0, room.1)).transformed(frame)
|
||||
}
|
||||
|
||||
/// What happens to the clip of an object a link update remakes.
|
||||
enum ClipTo {
|
||||
/// It stays as it is.
|
||||
Keep,
|
||||
/// It is clipped to this path: the clipping path of the clip group holding just it changes,
|
||||
/// else it goes into a new clip group.
|
||||
Path(PathData),
|
||||
/// It goes into a new clip group of this path, inside the clip group that holds it.
|
||||
Nest(PathData),
|
||||
/// The clipping path of the clip group holding just it goes (a DCS file's clipping path that
|
||||
/// its new file lacks): the group stays as a plain group of it, with its id, name, visibility
|
||||
/// and transparency.
|
||||
Off,
|
||||
}
|
||||
|
||||
/// The clipping path, with its fill rule, of the clip group holding just object `id` of `d`, when
|
||||
/// one holds it.
|
||||
fn clip_path_of(d: &Document, id: NodeId) -> Option<(&PathData, FillRule)> {
|
||||
let g = d.node(d.parent_of(id)?)?;
|
||||
match &g.kind {
|
||||
NodeKind::Group { children, clip: true } => match &children[..] {
|
||||
[c, o] if o.id == id => match &c.kind {
|
||||
NodeKind::Path { path, rule, clipping: true, .. } => Some((path, *rule)),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Do `a` and `b` have the same subpaths, each closed or open alike, and the same anchors, each
|
||||
/// within a thousandth of a point?
|
||||
fn same_path(a: &PathData, b: &PathData) -> bool {
|
||||
let near = |p: vectorcraft_geom::Point, q: vectorcraft_geom::Point| p.distance(q) < 1e-3;
|
||||
a.anchor_count() == b.anchor_count()
|
||||
&& a.subpaths.len() == b.subpaths.len()
|
||||
&& a.subpaths.iter().zip(&b.subpaths).all(|(x, y)| x.closed == y.closed)
|
||||
&& a.anchors()
|
||||
.zip(b.anchors())
|
||||
.all(|((si, ai, x), (sj, aj, y))| si == sj && ai == aj && near(x.p, y.p) && near(x.h_in, y.h_in) && near(x.h_out, y.h_out))
|
||||
}
|
||||
|
||||
/// The clip an image placed with `old_xf` takes when it shows `img`, read from a file, placed with
|
||||
/// `new_xf`: Placement Options' clip `clip` when they clip it, else the clipping path a DCS file
|
||||
/// draws `img` through. When neither clips it, a clip group holding just it whose clipping path
|
||||
/// `held` is its old DCS file's (`old_clip`, in the old pixels, under the nonzero rule its clip
|
||||
/// groups get) goes.
|
||||
fn clip_to(
|
||||
old_xf: Affine,
|
||||
old_clip: Option<&PathData>,
|
||||
clip: Option<Clip>,
|
||||
img: &RasterImage,
|
||||
new_xf: Affine,
|
||||
held: Option<(&PathData, FillRule)>,
|
||||
) -> ClipTo {
|
||||
if let Some(clip) = clip {
|
||||
return ClipTo::Path(clip_shape(clip));
|
||||
}
|
||||
// Whether the clip group holding just the image is the old DCS file's: a clipping path whose
|
||||
// fill rule was changed in the document is not.
|
||||
let file_held = match (old_clip, held) {
|
||||
(Some(old), Some((held, rule))) => rule == FillRule::NonZero && same_path(&old.transformed(old_xf), held),
|
||||
_ => false,
|
||||
};
|
||||
match img.clip.as_ref().map(|path| path.transformed(new_xf)) {
|
||||
// A clipping mask made in the document stays, with the file's clip group inside it.
|
||||
Some(path) if held.is_some() && !file_held => ClipTo::Nest(path),
|
||||
Some(path) => ClipTo::Path(path),
|
||||
None if file_held => ClipTo::Off,
|
||||
None => ClipTo::Keep,
|
||||
}
|
||||
}
|
||||
|
||||
/// The object an object becomes, and what happens to its clip, given the clipping path of the clip
|
||||
/// group holding just it ([`ClipTo`]).
|
||||
type Remake<'a> = dyn FnMut(&Node, Option<(&PathData, FillRule)>) -> (Node, ClipTo) + 'a;
|
||||
|
||||
/// Remakes the linked objects outside the layer tree (in symbols, graph designs, patterns and
|
||||
/// opacity masks, where [`Document::node`] finds no ids) that `pick` holds for.
|
||||
struct Nested<'a> {
|
||||
pick: &'a dyn Fn(&Node) -> bool,
|
||||
/// The object an object becomes, and the clip it takes ([`placed_xf`]).
|
||||
make: &'a mut dyn FnMut(&Node) -> (Node, Option<Clip>),
|
||||
make: &'a mut Remake<'a>,
|
||||
/// The next node id for a clip group (taken from the document after the walk).
|
||||
next: u64,
|
||||
}
|
||||
@@ -622,12 +718,12 @@ impl Nested<'_> {
|
||||
if !(self.pick)(n) {
|
||||
return self.update(n, true);
|
||||
}
|
||||
let (new, clip) = (self.make)(n);
|
||||
let (new, clip) = (self.make)(n, None);
|
||||
let mut new = Arc::new(new);
|
||||
self.update(&mut new, true);
|
||||
*n = match clip {
|
||||
Some(clip) => Arc::new(self.clip_group(new, clip)),
|
||||
None => new,
|
||||
ClipTo::Path(path) | ClipTo::Nest(path) => Arc::new(self.clip_group(new, path)),
|
||||
ClipTo::Keep | ClipTo::Off => new,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -640,6 +736,8 @@ impl Nested<'_> {
|
||||
(!Arc::ptr_eq(&art, &m.art)).then_some(art)
|
||||
});
|
||||
let mut slots: Vec<(usize, Arc<Node>)> = vec![];
|
||||
// The object a clip group that loses its clipping path holds alone.
|
||||
let mut declipped = None;
|
||||
if let Some(children) = n.children() {
|
||||
// A clip group holding just the object: its clipping path changes.
|
||||
let clip_path = match (&n.kind, &children[..]) {
|
||||
@@ -655,32 +753,45 @@ impl Nested<'_> {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let (new, clip) = (self.make)(c);
|
||||
let held = clip_path.filter(|_| i == 1).and_then(|p| match &p.kind {
|
||||
NodeKind::Path { path, rule, .. } => Some((path, *rule)),
|
||||
_ => None,
|
||||
});
|
||||
let (new, clip) = (self.make)(c, held);
|
||||
let mut new = Arc::new(new);
|
||||
self.update(&mut new, true);
|
||||
match (clip, clip_path) {
|
||||
(None, _) => slots.push((i, new)),
|
||||
(Some(clip), Some(path)) if i == 1 => {
|
||||
(ClipTo::Keep, _) => slots.push((i, new)),
|
||||
(ClipTo::Path(clip), Some(path)) if i == 1 => {
|
||||
let mut path = Node::clone(path);
|
||||
if let NodeKind::Path { path: p, .. } = &mut path.kind {
|
||||
*p = clip_shape(clip);
|
||||
*p = clip;
|
||||
}
|
||||
slots.extend([(0, Arc::new(path)), (1, new)]);
|
||||
}
|
||||
(Some(clip), _) => {
|
||||
(ClipTo::Path(clip) | ClipTo::Nest(clip), _) => {
|
||||
let group = self.clip_group(new, clip);
|
||||
slots.push((i, Arc::new(group)));
|
||||
}
|
||||
(ClipTo::Off, Some(_)) if i == 1 => declipped = Some(new),
|
||||
(ClipTo::Off, _) => slots.push((i, new)),
|
||||
}
|
||||
}
|
||||
}
|
||||
if mask.is_none() && slots.is_empty() {
|
||||
if mask.is_none() && slots.is_empty() && declipped.is_none() {
|
||||
return;
|
||||
}
|
||||
let n = Arc::make_mut(n);
|
||||
if let (Some(art), Some(m)) = (mask, n.extras_if_any().and_then(|e| e.mask.as_mut())) {
|
||||
m.art = art;
|
||||
}
|
||||
// A clip group that stays as a plain group of just the object.
|
||||
if let Some(object) = declipped {
|
||||
if let NodeKind::Group { children, clip } = &mut n.kind {
|
||||
(*children, *clip) = (vec![object], false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(children) = n.children_mut() {
|
||||
for (i, c) in slots {
|
||||
if let Some(slot) = children.get_mut(i) {
|
||||
@@ -690,12 +801,12 @@ impl Nested<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A clip group of `object` clipped to `clip`, as [`clip_image`] makes one in the layers.
|
||||
fn clip_group(&mut self, object: Arc<Node>, clip: Clip) -> Node {
|
||||
/// A clip group of `object` clipped to `clip`, as [`clip_image_to`] makes one in the layers.
|
||||
fn clip_group(&mut self, object: Arc<Node>, clip: PathData) -> Node {
|
||||
let (path_id, group_id) = (NodeId(self.next), NodeId(self.next + 1));
|
||||
self.next += 2;
|
||||
let none = vectorcraft_color::Paint::None;
|
||||
let mut path = Node::path(path_id, clip_shape(clip), Appearance::basic(none.clone(), none, 0.0));
|
||||
let mut path = Node::path(path_id, clip, Appearance::basic(none.clone(), none, 0.0));
|
||||
if let NodeKind::Path { clipping, .. } = &mut path.kind {
|
||||
*clipping = true;
|
||||
}
|
||||
@@ -723,13 +834,13 @@ fn install_document(
|
||||
};
|
||||
if let Some(also) = also {
|
||||
let pick = |n: &Node| shows(n, also, true);
|
||||
let mut make = |n: &Node| {
|
||||
let mut make = |n: &Node, _: Option<(&PathData, FillRule)>| {
|
||||
let mut n = n.clone();
|
||||
let mut clip = None;
|
||||
let mut clip = ClipTo::Keep;
|
||||
if let NodeKind::PlacedDocument(p) = &mut n.kind {
|
||||
let (xf, c) = placed(p);
|
||||
(p.key, p.width, p.height, p.xf, p.link) = (src.key.clone(), w, h, xf, link.clone());
|
||||
clip = c;
|
||||
clip = c.map_or(ClipTo::Keep, |c| ClipTo::Path(clip_shape(c)));
|
||||
}
|
||||
(n, clip)
|
||||
};
|
||||
@@ -801,10 +912,9 @@ fn placed_xf(xf: Affine, old: (f64, f64), old_pt: (f64, f64), new: (f64, f64), n
|
||||
(frame * at * Affine::scale_non_uniform(size.0 / nw, size.1 / nh), (o.clip && over).then_some((frame, room)))
|
||||
}
|
||||
|
||||
/// Clip image `id` of `d` to `room` in `frame`: the clipping path of the clip group holding just
|
||||
/// it changes, else the image goes into a new clip group.
|
||||
fn clip_image(d: &mut Document, id: NodeId, clip: Clip) -> Result<()> {
|
||||
let path = clip_shape(clip);
|
||||
/// The clipping path of the clip group of `d` holding just object `id` becomes `path`; false when
|
||||
/// no such clip group holds it.
|
||||
fn set_clip_path(d: &mut Document, id: NodeId, path: PathData) -> bool {
|
||||
let clip_path = d.parent_of(id).and_then(|p| match &d.node(p)?.kind {
|
||||
NodeKind::Group { children, clip: true } => match &children[..] {
|
||||
[c, im] if im.id == id && matches!(c.kind, NodeKind::Path { clipping: true, .. }) => Some(c.id),
|
||||
@@ -812,12 +922,30 @@ fn clip_image(d: &mut Document, id: NodeId, clip: Clip) -> Result<()> {
|
||||
},
|
||||
_ => None,
|
||||
});
|
||||
if let Some(c) = clip_path {
|
||||
if let Some(Node { kind: NodeKind::Path { path: p, .. }, .. }) = d.node_mut(c) {
|
||||
*p = path;
|
||||
}
|
||||
let Some(c) = clip_path else { return false };
|
||||
if let Some(Node { kind: NodeKind::Path { path: p, .. }, .. }) = d.node_mut(c) {
|
||||
*p = path;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Clip image `id` of `d` to `room` in `frame`: the clipping path of the clip group holding just
|
||||
/// it changes, else the image goes into a new clip group.
|
||||
fn clip_image(d: &mut Document, id: NodeId, clip: Clip) -> Result<()> {
|
||||
clip_image_to(d, id, clip_shape(clip))
|
||||
}
|
||||
|
||||
/// Clip object `id` of `d` to `path`: the clipping path of the clip group holding just it changes,
|
||||
/// else it goes into a new clip group.
|
||||
fn clip_image_to(d: &mut Document, id: NodeId, path: PathData) -> Result<()> {
|
||||
if set_clip_path(d, id, path.clone()) {
|
||||
return Ok(());
|
||||
}
|
||||
new_clip_group(d, id, path)
|
||||
}
|
||||
|
||||
/// Put object `id` of `d` into a new clip group of `path`, where it is.
|
||||
fn new_clip_group(d: &mut Document, id: NodeId, path: PathData) -> Result<()> {
|
||||
let (par, index, _) = d.position(id).ok_or(EngineError::NoNode(id))?;
|
||||
let image = d.remove(id)?;
|
||||
let none = vectorcraft_color::Paint::None;
|
||||
@@ -839,7 +967,7 @@ pub(crate) fn set_image_from_path(d: &mut Document, id: NodeId, path: &str, doc_
|
||||
// the app connect to another computer.
|
||||
let path = fileio::document_path(path, doc_path).map_err(|r| EngineError::Other(format!("the image `{path}` isn't followed: {r}")))?;
|
||||
let bytes = read_file(&path)?;
|
||||
let read = (link_info(&path, &bytes), linked_image(&bytes)?);
|
||||
let read = (link_info(&path, &bytes), linked_image(&bytes, Some(&path))?);
|
||||
install(d, &[id], read, Some(Preserve::Bounds), None)
|
||||
}
|
||||
|
||||
@@ -853,15 +981,17 @@ fn install(d: &mut Document, ids: &[NodeId], (link, img): FileRead, force: Optio
|
||||
// Every image picked shows content `also.1`, so one resolution serves them all.
|
||||
let old_pt = file_pt(d, also.1);
|
||||
let pick = |n: &Node| shows(n, also, false);
|
||||
let mut make = |n: &Node| {
|
||||
// The clipping path the old content's DCS file drew it through.
|
||||
let old_clip = d.images.get(also.1).and_then(ImageBlob::separated_clip);
|
||||
let mut make = |n: &Node, held: Option<(&PathData, FillRule)>| {
|
||||
let mut n = n.clone();
|
||||
let mut clip = None;
|
||||
let mut clip = ClipTo::Keep;
|
||||
if let NodeKind::Image(im) = &mut n.kind {
|
||||
let o = PlacementOptions { preserve: force.unwrap_or(im.placement.preserve), ..im.placement };
|
||||
let size = |w: u32, h: u32| (w as f64, h as f64);
|
||||
let (xf, c) = placed_xf(im.xf, size(im.width, im.height), old_pt, size(img.width, img.height), new_pt, o);
|
||||
clip = clip_to(im.xf, old_clip.as_ref(), c, &img, xf, held);
|
||||
(im.key, im.width, im.height, im.xf, im.link) = (img.key.clone(), img.width, img.height, xf, Some(link.clone()));
|
||||
clip = c;
|
||||
}
|
||||
(n, clip)
|
||||
};
|
||||
@@ -872,15 +1002,43 @@ fn install(d: &mut Document, ids: &[NodeId], (link, img): FileRead, force: Optio
|
||||
let o = PlacementOptions { preserve: force.unwrap_or(im.placement.preserve), ..im.placement };
|
||||
let size = |w: u32, h: u32| (w as f64, h as f64);
|
||||
let (xf, clip) = placed_xf(im.xf, size(im.width, im.height), file_pt(d, &im.key), size(img.width, img.height), new_pt, o);
|
||||
// Placement Options' clip, else a DCS file's clipping path, read again with its plates.
|
||||
let old_clip = d.images.get(&im.key).and_then(ImageBlob::separated_clip);
|
||||
clips.push((*id, clip_to(im.xf, old_clip.as_ref(), clip, &img, xf, clip_path_of(d, *id))));
|
||||
if let Some(n) = d.node_mut(*id)
|
||||
&& let NodeKind::Image(im) = &mut n.kind
|
||||
{
|
||||
(im.key, im.width, im.height, im.xf, im.link) = (img.key.clone(), img.width, img.height, xf, Some(link.clone()));
|
||||
}
|
||||
clips.extend(clip.map(|c| (*id, c)));
|
||||
}
|
||||
// A DCS file's spot plates print on spot swatches of their inks.
|
||||
super::place::dcs::add_spot_swatches(d, &img.blob.spot_plates());
|
||||
store_image(d, &img.key, img.blob, true);
|
||||
clips.into_iter().try_for_each(|(id, clip)| clip_image(d, id, clip))
|
||||
clips.into_iter().try_for_each(|(id, clip)| match clip {
|
||||
ClipTo::Keep => Ok(()),
|
||||
ClipTo::Path(path) => clip_image_to(d, id, path),
|
||||
ClipTo::Nest(path) => new_clip_group(d, id, path),
|
||||
ClipTo::Off => unclip(d, id),
|
||||
})
|
||||
}
|
||||
|
||||
/// Take the clipping path of the clip group holding just object `id` of `d` away: the group stays
|
||||
/// as a plain group of the object, with its id, name, visibility and transparency as they were.
|
||||
fn unclip(d: &mut Document, id: NodeId) -> Result<()> {
|
||||
let Some((g, clip)) = d.parent_of(id).and_then(|g| match &d.node(g)?.kind {
|
||||
NodeKind::Group { children, clip: true } => match &children[..] {
|
||||
[c, o] if o.id == id && matches!(c.kind, NodeKind::Path { clipping: true, .. }) => Some((g, c.id)),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}) else {
|
||||
return Ok(());
|
||||
};
|
||||
d.remove(clip)?;
|
||||
if let Some(Node { kind: NodeKind::Group { clip, .. }, .. }) = d.node_mut(g) {
|
||||
*clip = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Groups (of the document saved at `doc_path`) as `{name, path, ids}` rows (those with objects in
|
||||
@@ -1013,7 +1171,7 @@ pub fn resolve(d: &mut Document, base: LinkBase, update: bool, low_res_proxy_eps
|
||||
// Read when the images show the preview, or when the file's details changed.
|
||||
let bytes = if (preview && !eps_preview) || f.bytes.is_some() { f.read().ok() } else { None };
|
||||
if preview && !eps_preview {
|
||||
let Some((b, img)) = bytes.as_deref().and_then(|b| Some((b, linked_image(b).ok()?))) else {
|
||||
let Some((b, img)) = bytes.as_deref().and_then(|b| Some((b, linked_image(b, Some(&f.path)).ok()?))) else {
|
||||
out.missing.extend(row);
|
||||
continue;
|
||||
};
|
||||
@@ -1051,12 +1209,12 @@ pub fn resolve(d: &mut Document, base: LinkBase, update: bool, low_res_proxy_eps
|
||||
m.iter().find(|(k, doc, _)| k == key && *doc == document.is_some()).map(|(_, _, new)| new)
|
||||
};
|
||||
let pick = |n: &Node| new(n).is_some();
|
||||
let mut make = |n: &Node| {
|
||||
let mut make = |n: &Node, _: Option<(&PathData, FillRule)>| {
|
||||
let mut out = n.clone();
|
||||
if let (Some(link), Some(l)) = (new(n), link_mut(&mut out)) {
|
||||
*l = link.clone();
|
||||
}
|
||||
(out, None)
|
||||
(out, ClipTo::Keep)
|
||||
};
|
||||
Nested { pick: &pick, make: &mut make, next: 0 }.run(d);
|
||||
}
|
||||
@@ -1328,10 +1486,44 @@ fn update_where(s: &mut Session, ids: Option<Vec<NodeId>>, path: Option<&str>) -
|
||||
}
|
||||
}
|
||||
let updated: Vec<u64> = reads.iter().flat_map(|(g, _)| ids_json(&g.ids)).collect();
|
||||
let warnings = spot_swatch_notes(&st.doc, reads.iter().map(|(_, read)| read));
|
||||
if !reads.is_empty() {
|
||||
s.edit("Update Links", |d, _| reads.into_iter().try_for_each(|(g, read)| install_read(d, &g.ids, read, None, Some(&g))))?;
|
||||
}
|
||||
Ok(json!({ "updated": updated, "missing": missing }))
|
||||
let mut out = json!({ "updated": updated, "missing": missing });
|
||||
if !warnings.is_empty() {
|
||||
out["warnings"] = json!(warnings);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The warnings for the spot plates of the DCS files in `reads` whose ink a swatch of `d` already
|
||||
/// names in another color, or as a process color ([`super::place::dcs::swatch_notes`]), or that an
|
||||
/// earlier file of `reads` adds as a spot swatch in another color, each once.
|
||||
fn spot_swatch_notes<'a>(d: &Document, reads: impl Iterator<Item = &'a Read>) -> Vec<String> {
|
||||
let mut out: Vec<String> = vec![];
|
||||
// The spot swatches the reads add, in the order they are installed.
|
||||
let mut added: Vec<(String, [f32; 4])> = vec![];
|
||||
for read in reads {
|
||||
let Read::Image((_, img)) = read else { continue };
|
||||
let spots = img.blob.spot_plates();
|
||||
let mut notes = super::place::dcs::swatch_notes(d, &spots);
|
||||
for (name, cmyk) in spots.into_iter().filter(|(name, _)| d.swatch(name).is_none()) {
|
||||
match added.iter().find(|(n, _)| *n == name) {
|
||||
Some((_, first)) if first.iter().zip(cmyk).any(|(a, b)| (a - b).abs() >= 0.005) => {
|
||||
notes.push(format!("the document's swatch `{name}` is kept: it is a different color"));
|
||||
}
|
||||
Some(_) => {}
|
||||
None => added.push((name, cmyk)),
|
||||
}
|
||||
}
|
||||
for note in notes {
|
||||
if !out.contains(¬e) {
|
||||
out.push(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Which links `links.relink` points at the new file (the Replace choice of Relink).
|
||||
@@ -1513,7 +1705,11 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
// Read before it is copied: a file that can't be shown isn't copied.
|
||||
let img = if images.is_empty() { None } else { Some(linked_image(&bytes).map_err(|e| bad(C, format!("{path}: {e}")))?) };
|
||||
let img = if images.is_empty() { None } else { Some(linked_image(&bytes, Some(&path)).map_err(|e| bad(C, format!("{path}: {e}")))?) };
|
||||
// A copy beside the document would point at plate files it can't find there.
|
||||
if copy_to.is_some() && img.as_ref().is_some_and(|img| !img.blob.plate_files().is_empty()) {
|
||||
return Err(bad(C, format!("{path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are")));
|
||||
}
|
||||
let mut sources = vec![];
|
||||
for ((page, bounding, page_box), ids) in documents {
|
||||
sources.push((page, ids, super::place::document::source(&path, &bytes, Some(&path), page, bounding, page_box, C)?));
|
||||
@@ -1548,12 +1744,16 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
let relinked: Vec<u64> = reads.iter().flat_map(|(ids, _)| ids_json(ids)).collect();
|
||||
let warnings = spot_swatch_notes(&s.doc()?.doc, reads.iter().map(|(_, read)| read));
|
||||
if !reads.is_empty() {
|
||||
s.edit("Relink", |d, _| reads.into_iter().try_for_each(|(ids, read)| install_read(d, &ids, read, preserve, None)))?;
|
||||
}
|
||||
not_found.sort();
|
||||
not_found.dedup();
|
||||
let mut out = json!({ "relinked": relinked, "notFound": not_found });
|
||||
if !warnings.is_empty() {
|
||||
out["warnings"] = json!(warnings);
|
||||
}
|
||||
if copy_to.is_some() {
|
||||
// The copies are in the document's folder, which the client did not give.
|
||||
for pair in copied.iter_mut().chain(kept.iter_mut()) {
|
||||
@@ -1712,6 +1912,9 @@ pub(crate) fn statuses(st: &crate::DocState) -> BTreeMap<NodeId, (Status, Option
|
||||
|
||||
/// The format of the file image `im` of `d` shows: its link's extension, else its pixels' type.
|
||||
fn format_label(d: &Document, im: &ImageObject) -> &'static str {
|
||||
if im.link.is_some() && d.images.get(&im.key).is_some_and(ImageBlob::is_separated) {
|
||||
return "DCS";
|
||||
}
|
||||
im.link
|
||||
.as_ref()
|
||||
.and_then(|l| fileio::format_for_name(l.name()).or_else(|| fileio::readable_format_for_name(l.name())))
|
||||
@@ -1732,6 +1935,45 @@ fn status_rank(status: &str) -> usize {
|
||||
["missing", "modified", "ok", "embedded"].iter().position(|s| *s == status).unwrap_or(4)
|
||||
}
|
||||
|
||||
/// The DCS images in the layers of `d` that are transparent (themselves, or a group or layer that
|
||||
/// holds them) or overlap an object that is, as Show DCS Transparency Interactions marks them.
|
||||
fn dcs_transparency(d: &Document) -> BTreeSet<NodeId> {
|
||||
/// Each visible DCS image's id, bounds and whether it is transparent, and the bounds of each
|
||||
/// other visible object with its own transparency.
|
||||
fn walk(d: &Document, n: &Node, held: bool, images: &mut Vec<(NodeId, Rect, bool)>, see_through: &mut Vec<Rect>) {
|
||||
if !n.visible {
|
||||
return;
|
||||
}
|
||||
let own = n.shows_own_transparency();
|
||||
match &n.kind {
|
||||
NodeKind::Image(im) if d.images.get(&im.key).is_some_and(ImageBlob::is_separated) => {
|
||||
if let Some(b) = n.geometric_bounds() {
|
||||
images.push((n.id, b, held || own));
|
||||
// A transparent DCS image is an object with its own transparency for the
|
||||
// others it overlaps.
|
||||
if own {
|
||||
see_through.push(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ if own => see_through.extend(n.geometric_bounds()),
|
||||
_ => {}
|
||||
}
|
||||
for c in n.children().into_iter().flatten() {
|
||||
walk(d, c, held || own, images, see_through);
|
||||
}
|
||||
}
|
||||
let (mut images, mut see_through) = (vec![], vec![]);
|
||||
for l in &d.layers {
|
||||
walk(d, l, false, &mut images, &mut see_through);
|
||||
}
|
||||
images
|
||||
.into_iter()
|
||||
.filter(|(_, b, transparent)| *transparent || see_through.iter().any(|t| t.intersect(*b).area() > 0.0))
|
||||
.map(|(id, ..)| id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "links.list";
|
||||
let st = s.doc()?;
|
||||
@@ -1739,6 +1981,7 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !matches!(show, "all" | "missing" | "modified" | "embedded") {
|
||||
return Err(bad(C, format!("show `{show}`: all, missing, modified or embedded")));
|
||||
}
|
||||
let interactions = if switch_or(p, "dcsTransparency", false, C)? { dcs_transparency(&st.doc) } else { BTreeSet::new() };
|
||||
let statuses = statuses(st);
|
||||
let mut objects = vec![];
|
||||
st.doc.walk(|n| {
|
||||
@@ -1793,6 +2036,9 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if preview {
|
||||
row["preview"] = json!(true);
|
||||
}
|
||||
if interactions.contains(&n.id) {
|
||||
row["dcsTransparency"] = json!(true);
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
let text = |r: &Value, k: &str| r[k].as_str().unwrap_or_default().to_lowercase();
|
||||
@@ -1831,6 +2077,14 @@ fn embed(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if ids.is_empty() {
|
||||
return Err(bad(C, "select the linked images or placed documents, or pass ids"));
|
||||
}
|
||||
// A DCS file's plates print from its file.
|
||||
let separated = |id: &NodeId| match kind(id) {
|
||||
Some(NodeKind::Image(im)) => st.doc.images.get(&im.key).is_some_and(ImageBlob::is_separated),
|
||||
_ => false,
|
||||
};
|
||||
if let Some(id) = ids.iter().find(|id| separated(id)) {
|
||||
return Err(bad(C, format!("object {}: {}", id.0, super::place::dcs::LINK_ONLY)));
|
||||
}
|
||||
// Placed documents become editable copies of their art: those whose file can be read.
|
||||
let (mut documents, mut missing) = (vec![], vec![]);
|
||||
for id in ids.iter().copied() {
|
||||
@@ -1849,6 +2103,10 @@ fn embed(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
match probe(&g, LinkBase::of(st)) {
|
||||
(Status::Ok | Status::Modified, Some(mut f)) => match read_image(&mut f, &g.link, &mut image_reads) {
|
||||
// One showing its preview is a DCS file only its file tells.
|
||||
Ok((_, img)) if img.blob.is_separated() => {
|
||||
return Err(bad(C, format!("object {}: {}", g.ids.first().map_or(0, |id| id.0), super::place::dcs::LINK_ONLY)));
|
||||
}
|
||||
Ok(read) => {
|
||||
done.extend(&g.ids);
|
||||
reads.push((g.ids, read));
|
||||
@@ -1915,7 +2173,7 @@ fn unembed(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let path = absolute_path(&if fileio::extension(path).is_empty() { format!("{path}.{ext}") } else { path.to_string() });
|
||||
// Read before writing: pixels that can't be read leave no file behind.
|
||||
let img = linked_image(&bytes)?;
|
||||
let img = linked_image(&bytes, Some(&path))?;
|
||||
write_file(&path, &bytes)?;
|
||||
let read = (link_info(&path, &bytes), img);
|
||||
s.edit("Unembed", |d, _| install(d, &[id], read, Some(Preserve::Bounds), None))?;
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Fill",
|
||||
[],
|
||||
None,
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}|{l,a,b} (CIE Lab; #rrggbbaa and #rgba are refused), none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect; the built-in \"[Registration]\" prints on every plate), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|conic|diamond|freeform (conic, also accepted as angle or conical: the stops sweep once around the center `start`, clockwise from the vector; diamond: the stops run from the center `start` out to a square with a corner at `end`), method?: classic|perceptual (as paint.editGradient; default classic), spread?: pad|repeat|reflect (as paint.editGradient; default pad), repeats?: 1..100 (as paint.editGradient; default 1), stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg counter-clockwise (0: the gradient runs left to right), start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial, conic and diamond; a negative one mirrors a conic's sweep, counterclockwise, and is an error for the other kinds; without start/end the gradient is placed on each object's bounds), focal?: [x,y] (radial, with start/end: the focal point, where the first stop sits; an error with a conic or diamond), dither?: bool (linear, radial, conic and diamond fills: drawn with fine noise that hides banding, and as pixels in SVG and PDF exports; strokes ignore it), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?|id?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray), target?: \"object\"|\"contents\", span?: \"object\" (default: each target fits a gradient to its own bounds)|\"selection\" (a linear, radial, conic or diamond `gradient` without start/end is placed once on the union of the targets' bounds, so it runs across them)} sets the selection's fill and the default (new art fits a gradient to itself); a solid colour recolours only the gradient mesh point last clicked with the Mesh tool or Direct Selection; while the Type tool has characters selected (and no ids or item are given), the paint goes to those characters only; without `item`, a grayscale or 1-bit image takes a solid color as the color it is drawn in (white pixels transparent; `none` takes it off; an image with an alpha channel takes only a spot color, black or white); with an object of a dynamic symbol instance selected by Direct Selection and no ids, id or item, that object's fill changes for this instance only (solid, none, gradient or pattern) → {objects: how many objects took the paint (the objects inside a group, unless an appearance item of the group's own is targeted; 0 when only the default changed)}",
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}|{l,a,b} (CIE Lab; #rrggbbaa and #rgba are refused), none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect; the built-in \"[Registration]\" prints on every plate), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|conic|diamond|freeform (conic, also accepted as angle or conical: the stops sweep once around the center `start`, clockwise from the vector; diamond: the stops run from the center `start` out to a square with a corner at `end`), method?: classic|perceptual (as paint.editGradient; default classic), spread?: pad|repeat|reflect (as paint.editGradient; default pad), repeats?: 1..100 (as paint.editGradient; default 1), stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg counter-clockwise (0: the gradient runs left to right), start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial, conic and diamond; a negative one mirrors a conic's sweep, counterclockwise, and is an error for the other kinds; without start/end the gradient is placed on each object's bounds), focal?: [x,y] (radial, with start/end: the focal point, where the first stop sits; an error with a conic or diamond), dither?: bool (linear, radial, conic and diamond fills: drawn with fine noise that hides banding, and as pixels in SVG and PDF exports; strokes ignore it), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?|id?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray), target?: \"object\"|\"contents\", span?: \"object\" (default: each target fits a gradient to its own bounds)|\"selection\" (a linear, radial, conic or diamond `gradient` without start/end is placed once on the union of the targets' bounds, so it runs across them)} sets the selection's fill and the default (new art fits a gradient to itself); a solid colour recolours only the gradient mesh point last clicked with the Mesh tool or Direct Selection; while the Type tool has characters selected (and no ids or item are given), the paint goes to those characters only; without `item`, a grayscale or 1-bit image takes a solid color as the color it is drawn in (white pixels transparent; `none` takes it off; an image with an alpha channel takes only a spot color, black or white; a colorized image whose link is missing takes a solid color as its new colorize color, and refuses a process color while its color is a spot color, black or white); with an object of a dynamic symbol instance selected by Direct Selection and no ids, id or item, that object's fill changes for this instance only (solid, none, gradient or pattern) → {objects: how many objects took the paint (the objects inside a group, unless an appearance item of the group's own is targeted; 0 when only the default changed)}",
|
||||
has_doc,
|
||||
|s, p| set_paint(s, p, true)
|
||||
),
|
||||
@@ -690,7 +690,9 @@ fn placed_across(d: &Document, ids: &[NodeId], paint: Paint, p: &Value, fill: bo
|
||||
/// The images among `ids` that a fill of `paint` colorizes: grayscale and 1-bit images, for a solid
|
||||
/// color (or none on a colorized image, which takes the color off). The color may be black, white,
|
||||
/// a process color or a spot color; on an image with an alpha channel, only a spot color, black or
|
||||
/// white, and other colors are an error.
|
||||
/// white, and other colors are an error. A colorized image that shows a preview that isn't
|
||||
/// grayscale, such as a missing link's placeholder, takes a solid color as its new colorize paint;
|
||||
/// while its paint is one an alpha channel allows, a process color is an error there too.
|
||||
fn colorized_images(d: &Document, ids: &[NodeId], paint: &Paint) -> std::result::Result<Vec<NodeId>, String> {
|
||||
let mut out = vec![];
|
||||
for id in ids {
|
||||
@@ -701,6 +703,19 @@ fn colorized_images(d: &Document, ids: &[NodeId], paint: &Paint) -> std::result:
|
||||
out.push(*id);
|
||||
continue;
|
||||
}
|
||||
// The Fill proxy shows such an image's colorize paint. Its pixels are not known until the
|
||||
// file is relinked: a process color replaces only a process color, which the image's
|
||||
// pixels already accepted, so a relink can't bring back an alpha channel under one.
|
||||
if matches!(paint, Paint::Solid { .. })
|
||||
&& let Some(old) = n.extras().colorize.as_deref()
|
||||
&& d.image_blob(im).is_some_and(|b| !b.takes_colorize())
|
||||
{
|
||||
if !colorizes_transparency(d, paint) && colorizes_transparency(d, old) {
|
||||
return Err(PROCESS_ON_TRANSPARENCY.into());
|
||||
}
|
||||
out.push(*id);
|
||||
continue;
|
||||
}
|
||||
// A color-masked image is colorized as it shows: its masked pixels, which have an alpha
|
||||
// channel.
|
||||
let Some(gray) = d.image_blob(&d.shown_image(n, im)).and_then(|b| b.gray_source()) else { continue };
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Close Path",
|
||||
[],
|
||||
None,
|
||||
"{id, subpath?: usize (the open subpath to close, 0 included; default: the last open one), in?: [x,y] (handle into the first anchor, placed as path.setHandle places it: a smooth first anchor keeps its outgoing handle in line at its own length, a corner keeps it where it is), out?: [x,y] (the first anchor's outgoing handle, placed after `in`; the anchor becomes a corner), independent?: bool (`in` makes the first anchor a corner and leaves its outgoing handle)}",
|
||||
"{id, subpath?: usize (the open subpath to close, 0 included; default: the last open one), at?: [x,y] (move the first anchor there, its handles with it, before `in` and `out` are placed), in?: [x,y] (handle into the first anchor, placed as path.setHandle places it: a smooth first anchor keeps its outgoing handle in line at its own length, a corner keeps it where it is), out?: [x,y] (the first anchor's outgoing handle, placed after `in`; the anchor becomes a corner), independent?: bool (`in` makes the first anchor a corner and leaves its outgoing handle)}",
|
||||
has_doc,
|
||||
close
|
||||
),
|
||||
@@ -205,6 +205,7 @@ fn append_anchor(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// Close the active open subpath of `id`, optionally setting the handles of the first anchor. The
|
||||
/// incoming handle moves as Move Direction Handle moves it: the outgoing handle turns with it only
|
||||
/// on a smooth anchor, so the first segment of a path that starts at a corner keeps its shape.
|
||||
/// `at` first moves the anchor, its handles with it (the Pen's closing drag with Space held).
|
||||
fn close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = object_param(p, "id", "path.close")?.ok_or_else(|| bad("path.close", "missing id"))?;
|
||||
let subpath = p.get("subpath").and_then(Value::as_u64).map(|u| u as usize);
|
||||
@@ -212,6 +213,7 @@ fn close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None => Ok(None),
|
||||
Some(h) => finite_numbers(h).map(|[x, y]| Some(Point::new(x, y))).ok_or_else(|| bad("path.close", format!("`{key}` must be [x, y]"))),
|
||||
};
|
||||
let at = handle("at")?;
|
||||
let h_in = handle("in")?;
|
||||
let h_out = handle("out")?;
|
||||
let independent = switch_or(p, "independent", false, "path.close")?;
|
||||
@@ -223,6 +225,9 @@ fn close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
sp.closed = true;
|
||||
if let Some(first) = sp.anchors.first_mut() {
|
||||
if let Some(to) = at {
|
||||
first.translate(to - first.p);
|
||||
}
|
||||
if let Some(h) = h_in {
|
||||
first.set_handle(false, h, independent);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
//! - An EPS file (or a PostScript .ai) becomes one group, clipped to its bounding box or bounded by
|
||||
//! its art: the document an EPS file of ours carries, else what its PostScript draws, else its
|
||||
//! preview ([`fileio::load`]).
|
||||
//! - A DCS file, an EPS file with one program per printing plate, becomes one linked image of its
|
||||
//! plates ([`dcs`]); it is never embedded.
|
||||
//! - A PDF/.ai/.ait page or a native document's artboard becomes one group, clipped to the page
|
||||
//! (`crop: "crop"`; a PDF page's `art`, `trim`, `bleed` or `media` box too) or bounded by its art
|
||||
//! (`crop: "bounding"`). With Link, a PDF/.ai/.ait page placed from a file becomes a placed
|
||||
@@ -32,6 +34,7 @@
|
||||
//! `place` tool) with several files, which emits one `file.place` per click or drag.
|
||||
|
||||
pub(crate) mod adopt;
|
||||
pub(crate) mod dcs;
|
||||
mod doc;
|
||||
pub(crate) mod document;
|
||||
pub(crate) mod docx;
|
||||
@@ -69,7 +72,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Place…",
|
||||
["File"],
|
||||
Some("Cmd+Shift+P"),
|
||||
"{path | name | dataBase64 (a file's path, or its name with its bytes), link?: true (a raster image keeps its file's path; a VectorCraft document at path becomes one locked placed document showing artboard `page`, linked to the file and read again when it changes (links.update; saving it in this app updates it at once), drawn as vectors in every output; so does a PDF/.ai page at path, showing its `crop` box, unless password or textAs: \"outlines\" is given; false: an editable copy of its art, as from dataBase64; other files are embedded), text?: {characterSet?: \"unicode\" (UTF-8, or UTF-16 with a byte-order mark; other bytes as the platform's 8-bit set) | \"ansi\" (the platform's 8-bit set), platform?: \"windows\" (Windows-1252) | \"mac\" (Mac Roman), removeLineReturns?: false (each block of lines becomes one paragraph; blank lines end paragraphs), removeParagraphReturns?: false (drop blank lines), replaceSpaces?: n (runs of n ≥ 2 spaces become a tab)} (a .txt file, placed as area type filling rect, the replaced object's bounds, or else the artboard less a 36 pt margin), template?: false (onto a new locked template layer below the current layer), replace?: false (swap the one selected object, keeping its stacking place and transform; no at/rect; refused while an object of a symbol instance's art is selected), at?: [x, y] centre (default: the first artboard's centre), rect?: [x, y, width, height] fit inside; aspect kept (wins over at), page?: 1 (PDF/.ai page, or a native document's artboard), crop?: \"crop\" (clipped to that page or artboard, default; an SVG, only when given: framed and clipped by its viewBox, else by its art's bounds) | \"bounding\" (the bounds of the art that shows, with its strokes and effects, as an export measures them: hidden objects and guides left out) | \"art\" | \"trim\" | \"bleed\" | \"media\" (a PDF page's boxes), password? (an encrypted PDF), dxf?: {…the DXF options of document.open} (a .dxf drawing: fit fills the artboard under at, else the first; fitted or uncentred art lands with its drawing's artboard on that artboard, else it is centred on at)} → {ids, name, format, linked, width, height, warnings}. Raster images come in at 100% of their physical size (the file's ppi, else 72); SVG, DXF, HP-GL (.plt, .hpgl, .hgl, .hpg), PDF/.ai, EMF/WMF (clipped to the picture's frame), EPS (clipped to its bounding box; as document.open reads it), a .ai saved without PDF compatibility (from its editing data, as document.open reads it: the art of the layers that show on artboard `page`, clipped to that artboard unless crop is \"bounding\") and native documents as one group, with the images, symbols, patterns, swatches and brushes they use. One undo step; selects what it placed (unless on a template layer); never touches the clipboard. A PNG, JPEG, GIF or WebP image whose pixels can't be read (a file cut short) is an error, and nothing is placed",
|
||||
"{path | name | dataBase64 (a file's path, or its name with its bytes), link?: true (a raster image keeps its file's path; a VectorCraft document at path becomes one locked placed document showing artboard `page`, linked to the file and read again when it changes (links.update; saving it in this app updates it at once), drawn as vectors in every output; so does a PDF/.ai page at path, showing its `crop` box, unless password or textAs: \"outlines\" is given; false: an editable copy of its art, as from dataBase64; other files are embedded), text?: {characterSet?: \"unicode\" (UTF-8, or UTF-16 with a byte-order mark; other bytes as the platform's 8-bit set) | \"ansi\" (the platform's 8-bit set), platform?: \"windows\" (Windows-1252) | \"mac\" (Mac Roman), removeLineReturns?: false (each block of lines becomes one paragraph; blank lines end paragraphs), removeParagraphReturns?: false (drop blank lines), replaceSpaces?: n (runs of n ≥ 2 spaces become a tab)} (a .txt file, placed as area type filling rect, the replaced object's bounds, or else the artboard less a 36 pt margin), template?: false (onto a new locked template layer below the current layer), replace?: false (swap the one selected object, keeping its stacking place and transform; no at/rect; refused while an object of a symbol instance's art is selected), at?: [x, y] centre (default: the first artboard's centre), rect?: [x, y, width, height] fit inside; aspect kept (wins over at), page?: 1 (PDF/.ai page, or a native document's artboard), crop?: \"crop\" (clipped to that page or artboard, default; an SVG, only when given: framed and clipped by its viewBox, else by its art's bounds) | \"bounding\" (the bounds of the art that shows, with its strokes and effects, as an export measures them: hidden objects and guides left out) | \"art\" | \"trim\" | \"bleed\" | \"media\" (a PDF page's boxes), password? (an encrypted PDF), dxf?: {…the DXF options of document.open} (a .dxf drawing: fit fills the artboard under at, else the first; fitted or uncentred art lands with its drawing's artboard on that artboard, else it is centred on at)} → {ids, name, format, linked, width, height, warnings, image?: the id of the image of a DCS file placed in a clip group, which the Links commands take}. Raster images come in at 100% of their physical size (the file's ppi, else 72); SVG, DXF, HP-GL (.plt, .hpgl, .hgl, .hpg), PDF/.ai, EMF/WMF (clipped to the picture's frame), EPS (clipped to its bounding box; as document.open reads it), a .ai saved without PDF compatibility (from its editing data, as document.open reads it: the art of the layers that show on artboard `page`, clipped to that artboard unless crop is \"bounding\") and native documents as one group, with the images, symbols, patterns, swatches and brushes they use. A DCS file (an EPS file with one program per printing plate, DCS 1.0 or 2.0) is placed only from its path with link (else an error, as are document.open and links.embed): one linked image of its plates mixed into CMYK, clipped by the clipping path its composite is drawn through, its spot plates added as spot swatches in the process equivalents the file gives (a swatch of the same name is kept, with a warning); print separations and Separations Preview show each plate on its own ink, and plate files of their own are read from its folder. One undo step; selects what it placed (unless on a template layer); never touches the clipboard. A PNG, JPEG, GIF or WebP image whose pixels can't be read (a file cut short) is an error, and nothing is placed",
|
||||
has_doc,
|
||||
place
|
||||
),
|
||||
@@ -78,7 +81,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Placed File Info",
|
||||
[],
|
||||
None,
|
||||
"{path | name | dataBase64 (a file's path, or its name with its bytes), page?, crop?, password?, thumbnail?: px, link?: bool, dxf?: {…} (both as file.place)} what file.place would place, without placing it → {name, format, width, height (pt at 100%), pixelWidth?, pixelHeight?, ppi?: [x, y], colorMode?: RGB|Grayscale|CMYK (raster images), warnings, thumbnailBase64?: PNG of at most `thumbnail` (≤ 512) px on its longer side}",
|
||||
"{path | name | dataBase64 (a file's path, or its name with its bytes), page?, crop?, password?, thumbnail?: px, link?: bool, dxf?: {…} (both as file.place)} what file.place would place, without placing it → {name, format, width, height (pt at 100%), pixelWidth?, pixelHeight?, ppi?: [x, y], colorMode?: RGB|Grayscale|CMYK (raster images), plates?: [ink names] (a DCS file), warnings, thumbnailBase64?: PNG of at most `thumbnail` (≤ 512) px on its longer side}",
|
||||
always,
|
||||
info
|
||||
),
|
||||
@@ -114,6 +117,8 @@ enum Art {
|
||||
/// A VectorCraft document or a PDF/.ai page placed linked: the file read, the link, and
|
||||
/// whether the art's bounds (not the artboard or page) are shown.
|
||||
Document(document::Source, LinkInfo, bool),
|
||||
/// A DCS file: one linked image of its plates, clipped by its clipping path ([`dcs`]).
|
||||
Dcs(Box<dcs::DcsArt>),
|
||||
Vector {
|
||||
/// The loaded document: the source of the art's resources.
|
||||
src: Box<Document>,
|
||||
@@ -205,6 +210,15 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>, link_pdf: bool) -> Result<Loa
|
||||
));
|
||||
}
|
||||
let crop = opts.crop != CropTo::Bounding;
|
||||
// A DCS file stays linked to its file, which its plates print from; its parameters are checked
|
||||
// as an EPS file's are.
|
||||
if dcs::is_dcs(&src.bytes) {
|
||||
let path = src.path.filter(|_| bool_or(p, "link", true)).map(fileio::absolute_path).ok_or_else(|| bad(cmd, dcs::LINK_ONLY))?;
|
||||
let a = dcs::read(&src.bytes, Some(&path)).map_err(|e| bad(cmd, format!("{name}: {e}")))?;
|
||||
let link = super::links::link_info(&path, &src.bytes);
|
||||
let (natural, warnings) = (a.page, a.warnings.clone());
|
||||
return Ok(Loaded { name, format, art: Art::Dcs(Box::new(a)), natural, warnings, link: Some(link), board: None });
|
||||
}
|
||||
// A VectorCraft document: placed linked from its file, else as an editable copy.
|
||||
if matches!(format.id, "vectorcraft" | "template") {
|
||||
let page = page as u32;
|
||||
@@ -408,6 +422,21 @@ fn build(d: &mut Document, l: Loaded, link: Option<LinkInfo>) -> Node {
|
||||
};
|
||||
Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)))
|
||||
}
|
||||
Art::Dcs(a) => {
|
||||
let a = *a;
|
||||
super::links::store_image(d, &a.img.key, a.img.blob, link.is_some());
|
||||
dcs::add_spot_swatches(d, &a.spots);
|
||||
let im =
|
||||
ImageObject { key: a.img.key, width: a.img.width, height: a.img.height, xf: a.xf, link, placement: Default::default(), pixels: None };
|
||||
let image = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
match a.clip {
|
||||
Some(c) => {
|
||||
let children = vec![Arc::new(d.reid(&c)), Arc::new(image)];
|
||||
Node::new(d.alloc_id(), NodeKind::Group { children, clip: true })
|
||||
}
|
||||
None => image,
|
||||
}
|
||||
}
|
||||
Art::Text(t) => Node::new(d.alloc_id(), NodeKind::Text(Box::new(text::area_text(t, l.natural, &d.setup)))),
|
||||
Art::Document(s, link, bounding) => document::node(d, &s, link, bounding),
|
||||
Art::Vector { src, mut nodes, clip } => {
|
||||
@@ -506,9 +535,13 @@ impl PlaceJob {
|
||||
pub fn read(self) -> Result<PlaceRead> {
|
||||
let mut loaded = load(&self.params, PLACE, self.target, true)?;
|
||||
let linked = loaded.link.is_some() && bool_or(&self.params, "link", true);
|
||||
if let Art::Image(img) = &mut loaded.art {
|
||||
let unread = super::links::kept_unread(&img.blob.bytes);
|
||||
img.blob = super::links::readable_and_kept(img.blob.clone(), unread, linked)?;
|
||||
match &mut loaded.art {
|
||||
Art::Image(img) => {
|
||||
let unread = super::links::kept_unread(&img.blob.bytes);
|
||||
img.blob = super::links::readable_and_kept(img.blob.clone(), unread, linked)?;
|
||||
}
|
||||
Art::Dcs(a) => a.img.blob = super::links::readable(a.img.blob.clone(), false, true)?,
|
||||
_ => {}
|
||||
}
|
||||
Ok(PlaceRead { job: self, loaded })
|
||||
}
|
||||
@@ -628,7 +661,11 @@ fn apply(s: &mut Session, read: PlaceRead) -> Result<Value> {
|
||||
let below = st.current_layer().or_else(|| parent.and_then(|p| st.doc.layer_of(p)));
|
||||
let link = loaded.link.take().filter(|_| bool_or(p, "link", true));
|
||||
let linked = link.is_some();
|
||||
let (name, format, warnings) = (loaded.name.clone(), loaded.format.id, std::mem::take(&mut loaded.warnings));
|
||||
let (name, format, mut warnings) = (loaded.name.clone(), loaded.format.id, std::mem::take(&mut loaded.warnings));
|
||||
let dcs_clipped = matches!(&loaded.art, Art::Dcs(a) if a.clip.is_some());
|
||||
if let Art::Dcs(a) = &loaded.art {
|
||||
warnings.extend(dcs::swatch_notes(&st.doc, &a.spots));
|
||||
}
|
||||
let (id, size) = s.edit("Place", |d, sel| {
|
||||
let node = transformed(build(d, loaded, link), m);
|
||||
let size = node.geometric_bounds_with(&d.symbol_boxes()).unwrap_or_default();
|
||||
@@ -650,9 +687,18 @@ fn apply(s: &mut Session, read: PlaceRead) -> Result<Value> {
|
||||
}
|
||||
Ok((id, size))
|
||||
})?;
|
||||
Ok(
|
||||
json!({ "ids": [id.0], "name": name, "format": format, "linked": linked, "width": size.width(), "height": size.height(), "warnings": warnings }),
|
||||
)
|
||||
let mut out = json!({ "ids": [id.0], "name": name, "format": format, "linked": linked, "width": size.width(), "height": size.height(), "warnings": warnings });
|
||||
// A DCS file clipped by its clipping path: its image, which the Links commands take.
|
||||
if dcs_clipped {
|
||||
let image = s.doc()?.doc.node(id).and_then(|n| match &n.kind {
|
||||
NodeKind::Group { children, clip: true } => children.get(1).filter(|c| matches!(c.kind, NodeKind::Image(_))).map(|c| c.id),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(image) = image {
|
||||
out["image"] = json!(image.0);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The artboard under `at` (its edges included, [`Document::artboard_holding`]), else the first.
|
||||
@@ -750,10 +796,14 @@ fn read_queue(p: Value, queued: Vec<Value>) -> Result<QueueRead> {
|
||||
// so that drawing it does not decode the file again.
|
||||
let checked = load(&q, QUEUE, None, false).and_then(|mut l| {
|
||||
let linked = l.link.is_some() && bool_or(&q, "link", true);
|
||||
if let Art::Image(img) = &mut l.art {
|
||||
let unread = super::links::kept_unread(&img.blob.bytes);
|
||||
let check = if thumbnail.is_some() { super::links::readable_and_kept } else { super::links::readable };
|
||||
img.blob = check(img.blob.clone(), unread, linked)?;
|
||||
match &mut l.art {
|
||||
Art::Image(img) => {
|
||||
let unread = super::links::kept_unread(&img.blob.bytes);
|
||||
let check = if thumbnail.is_some() { super::links::readable_and_kept } else { super::links::readable };
|
||||
img.blob = check(img.blob.clone(), unread, linked)?;
|
||||
}
|
||||
Art::Dcs(a) => a.img.blob = super::links::readable(a.img.blob.clone(), false, true)?,
|
||||
_ => {}
|
||||
}
|
||||
Ok(l)
|
||||
});
|
||||
@@ -871,6 +921,15 @@ pub fn read_info(p: &Value) -> Result<Value> {
|
||||
out["ppi"] = json!([x, y]);
|
||||
out["colorMode"] = json!(color_mode(&img.blob.bytes));
|
||||
}
|
||||
if let Art::Dcs(a) = &l.art {
|
||||
let (x, y) = a.img.ppi.unwrap_or((72.0, 72.0));
|
||||
out["pixelWidth"] = json!(a.img.width);
|
||||
out["pixelHeight"] = json!(a.img.height);
|
||||
out["ppi"] = json!([x, y]);
|
||||
out["colorMode"] = json!("CMYK");
|
||||
let plates: Vec<String> = a.img.blob.separated().map(|s| s.plates.iter().map(|p| p.name.clone()).collect()).unwrap_or_default();
|
||||
out["plates"] = json!(plates);
|
||||
}
|
||||
if let Some(px) = p.get("thumbnail").and_then(Value::as_f64) {
|
||||
out["thumbnailBase64"] = json!(thumbnail_png(l, px)?);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//! DCS files (`file.place` with Link): an EPS file with one program per printing plate
|
||||
//! ([`vectorcraft_eps::dcs`]). It is placed as one linked image whose plates are kept with its
|
||||
//! pixels ([`vectorcraft_doc::separated`]): the screen and composite output show the plates mixed
|
||||
//! into CMYK, and print separations put each plate on its own ink. Its spot plates become spot
|
||||
//! swatches, and the clipping path its composite is drawn through clips it. A DCS file can't be
|
||||
//! opened or embedded (Embed, Place without Link): the image stays linked to its file, which the
|
||||
//! Links commands read again ([`raster`]).
|
||||
|
||||
use vectorcraft_color::swatch::Swatch;
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::separated::PlateFile;
|
||||
use vectorcraft_doc::{Document, ImageBlob, Node, NodeKind};
|
||||
use vectorcraft_geom::{Affine, Rect};
|
||||
|
||||
use super::super::fileio::{self, RasterImage};
|
||||
use super::super::{EngineError, Result};
|
||||
|
||||
/// The largest plate file read (bytes).
|
||||
const MAX_PLATE_FILE: u64 = 1 << 30;
|
||||
|
||||
/// What refuses to open or embed a DCS file.
|
||||
pub(crate) const LINK_ONLY: &str = "DCS files can only be placed linked: use File › Place with Link on";
|
||||
|
||||
/// A DCS file read for placing.
|
||||
pub(crate) struct DcsArt {
|
||||
pub img: RasterImage,
|
||||
/// The image's pixels onto the file's page (its bounding box, top-left at the origin).
|
||||
pub xf: Affine,
|
||||
pub page: Rect,
|
||||
/// The clipping path the composite is drawn through, on the page.
|
||||
pub clip: Option<Node>,
|
||||
/// The spot plates' inks and their process equivalents.
|
||||
pub spots: Vec<(String, [f32; 4])>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Is `bytes` a DCS file (an EPS file naming its plates)?
|
||||
pub(crate) fn is_dcs(bytes: &[u8]) -> bool {
|
||||
vectorcraft_eps::dcs::read(bytes).is_some()
|
||||
}
|
||||
|
||||
/// DCS file `bytes`, read from `path` (when there is one: plate files of their own are found in its
|
||||
/// folder), with every plate.
|
||||
pub(crate) fn read(bytes: &[u8], path: Option<&str>) -> Result<DcsArt> {
|
||||
let dcs = vectorcraft_eps::dcs::read(bytes).ok_or_else(|| EngineError::Other("not a DCS file".into()))?;
|
||||
let mut files = vec![];
|
||||
let mut file = |name: &str| plate_file(path, name, &mut files);
|
||||
let p = vectorcraft_eps::dcs::place(bytes, &dcs, &mut file).map_err(EngineError::Other)?;
|
||||
// The clipping path in the image's pixel space, where the Links commands put it again; the
|
||||
// image keeps it, so they know a clip group of that path as the file's.
|
||||
let to_pixels = (p.xf.determinant().abs() > 1e-12).then(|| p.xf.inverse());
|
||||
let clip = match (&p.clip, to_pixels) {
|
||||
(Some(Node { kind: NodeKind::Path { path, .. }, .. }), Some(inv)) => Some(path.transformed(inv)),
|
||||
_ => None,
|
||||
};
|
||||
let blob = ImageBlob::separated_tiff_clipped(&p.separated, p.ppi, &files, clip.as_ref())
|
||||
.ok_or_else(|| EngineError::Other("the plates can't be kept".into()))?;
|
||||
let img = RasterImage { key: blob.content_key(), blob, width: p.separated.width, height: p.separated.height, ppi: p.ppi, clip };
|
||||
let spots = p.separated.spots().map(|s| (s.name.clone(), s.cmyk)).collect();
|
||||
Ok(DcsArt { img, xf: p.xf, page: p.page, clip: p.clip, spots, warnings: p.warnings })
|
||||
}
|
||||
|
||||
/// A linked file's image, as the Links commands read it again: a DCS file's plates
|
||||
/// ([`read`]), else the image file ([`fileio::raster_image`]).
|
||||
pub(crate) fn raster(bytes: &[u8], path: Option<&str>) -> Result<RasterImage> {
|
||||
if is_dcs(bytes) { read(bytes, path).map(|a| a.img) } else { fileio::raster_image(bytes) }
|
||||
}
|
||||
|
||||
/// The path of the plate file a DCS file at `main` names `name`: the file of that name (its last
|
||||
/// component, of any system's path) in `main`'s folder, with that name.
|
||||
fn plate_path(main: Option<&str>, name: &str) -> std::result::Result<(String, String), String> {
|
||||
let base = name.rsplit(['/', '\\', ':']).next().unwrap_or(name);
|
||||
let folder = main.map(std::path::Path::new).and_then(std::path::Path::parent);
|
||||
let Some(folder) = folder.filter(|_| !base.is_empty()) else {
|
||||
return Err(format!("`{name}` is a file of its own, which can only be found next to the DCS file's path"));
|
||||
};
|
||||
Ok((folder.join(base).to_string_lossy().into_owned(), base.to_string()))
|
||||
}
|
||||
|
||||
/// The path of the plate file named `name` that the DCS file at `main` (a path as a link keeps
|
||||
/// it) was read with ([`plate_path`]); `None` when `main` has no folder.
|
||||
pub(crate) fn plate_file_path(main: &str, name: &str) -> Option<String> {
|
||||
plate_path(Some(main), name).ok().map(|(path, _)| path)
|
||||
}
|
||||
|
||||
/// The plate file a DCS file at `main` names `name` ([`plate_path`]), noted in `files` as it is now.
|
||||
fn plate_file(main: Option<&str>, name: &str, files: &mut Vec<PlateFile>) -> std::result::Result<Vec<u8>, String> {
|
||||
let (path, base) = plate_path(main, name)?;
|
||||
match fileio::file_stamp(&path) {
|
||||
None => Err(format!("`{base}` is not in the DCS file's folder")),
|
||||
Some((size, _)) if size > MAX_PLATE_FILE => Err(format!("`{base}` is larger than {} MB", MAX_PLATE_FILE >> 20)),
|
||||
Some((size, modified)) => {
|
||||
let bytes = fileio::read_file(&path).map_err(|e| e.to_string())?;
|
||||
files.push(PlateFile { name: base, size, modified: modified.unwrap_or(0) });
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether one of `files`, the plate files of their own a separated image was read from
|
||||
/// ([`ImageBlob::plate_files`]), next to the DCS file at `main`, is gone or changed since.
|
||||
pub(crate) fn plate_files_changed(files: &[PlateFile], main: &str) -> bool {
|
||||
files.iter().any(|f| {
|
||||
let stamp = plate_path(Some(main), &f.name).ok().and_then(|(path, _)| fileio::file_stamp(&path));
|
||||
stamp.is_none_or(|(size, modified)| size != f.size || modified.unwrap_or(0) != f.modified)
|
||||
})
|
||||
}
|
||||
|
||||
/// Add `spots` to `d` as spot swatches, each whose name no swatch has, in its process equivalent.
|
||||
pub(crate) fn add_spot_swatches(d: &mut Document, spots: &[(String, [f32; 4])]) {
|
||||
for (name, [c, m, y, k]) in spots {
|
||||
if d.swatch(name).is_none() {
|
||||
d.swatches.push(Swatch { name: name.clone(), paint: Paint::solid(Color::cmyk(*c, *m, *y, *k)), global: true, spot: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A warning for each of `spots` whose name a swatch of `d` already has that isn't a spot color in
|
||||
/// the same process equivalent: the document's swatch stays, and the plate prints by it.
|
||||
pub(crate) fn swatch_notes(d: &Document, spots: &[(String, [f32; 4])]) -> Vec<String> {
|
||||
let mut out = vec![];
|
||||
for (name, cmyk) in spots {
|
||||
let Some(sw) = d.swatch(name) else { continue };
|
||||
let same = sw.spot
|
||||
&& matches!(sw.paint.color(), Some(Color::Cmyk { c, m, y, k }) if [c, m, y, k].iter().zip(cmyk).all(|(a, b)| (a - b).abs() < 0.005));
|
||||
if !same {
|
||||
let how = if sw.spot { "a different color" } else { "a process color, so the plate prints as process inks" };
|
||||
out.push(format!("the document's swatch `{name}` is kept: it is {how}"));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -528,6 +528,7 @@ pub(super) const ALLOWED: &[&str] = &[
|
||||
"select.same.textStrokeColor",
|
||||
"select.segments",
|
||||
"select.set",
|
||||
"select.storedPath",
|
||||
"select.toggle",
|
||||
"select.update",
|
||||
"shape.arc",
|
||||
|
||||
@@ -112,6 +112,9 @@ fn preview(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
super::printadvanced::flattener(C, &set, &s.prefs.flattener_presets)?;
|
||||
let mut pv = vectorcraft_pdf::preview(doc, &set).map_err(|e| pdf_error(C, e))?;
|
||||
pv.warnings.extend(type_warnings(doc, set.print_layers, &areas(&pv)));
|
||||
// What the job loses comes first, then the notes on the options.
|
||||
let settings = std::mem::take(&mut pv.setting_notes);
|
||||
pv.warnings.extend(settings);
|
||||
let mut v = serde_json::to_value(pv).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
v["settings"] = to_json(&set)?;
|
||||
Ok(v)
|
||||
@@ -199,7 +202,9 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut r =
|
||||
vectorcraft_pdf::print(printed, &PrintOptions { settings: set, for_printer: bool_or(p, "forPrinter", false), ..Default::default() })
|
||||
.map_err(|e| pdf_error(PRINT, e))?;
|
||||
// What the job loses comes first, then the notes on the options.
|
||||
r.warnings.extend(typed);
|
||||
r.warnings.extend(r.setting_notes);
|
||||
return write_or_return(path, &r.bytes, json!({ "pages": r.pages, "format": "pdf", "warnings": r.warnings }));
|
||||
}
|
||||
let level = match p.get("level").filter(|v| !v.is_null()) {
|
||||
@@ -209,12 +214,13 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
None => Level::default(),
|
||||
};
|
||||
let mut warnings = vec![];
|
||||
// What the job loses, then the notes on the options: a reply lists them in that order.
|
||||
let (mut warnings, mut setting_notes) = (vec![], vec![]);
|
||||
if !bitmap && (tile_images || doc.layers.iter().any(|l| l.shows_transparency())) {
|
||||
warnings.push("transparency is flattened into opaque art and images (PostScript has none): see flattenerPreset".to_string());
|
||||
}
|
||||
if !set.color.profile.trim().is_empty() && set.output.mode == vectorcraft_pdf::OutputMode::Composite {
|
||||
warnings.push("the printer profile converts the colours of PDF output: PostScript prints the document's colours".to_string());
|
||||
setting_notes.push("the printer profile converts the colours of PDF output: PostScript prints the document's colours".to_string());
|
||||
}
|
||||
let flatness = (!set.graphics.auto_flatness).then_some(set.graphics.flatness);
|
||||
let plan = vectorcraft_pdf::plan(prepared.as_ref().unwrap_or(doc), &PrintOptions { settings: set, postscript: true, ..Default::default() })
|
||||
@@ -244,5 +250,11 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let areas: Vec<Rect> = plan.sheets.iter().filter_map(|sh| on_page(sh.area, sh.view * sh.place, sh.size)).collect();
|
||||
warnings.extend(type_warnings(&full, which, &areas));
|
||||
for w in plan.setting_notes.iter().chain(&out.setting_notes) {
|
||||
if !warnings.contains(w) && !setting_notes.contains(w) {
|
||||
setting_notes.push(w.clone());
|
||||
}
|
||||
}
|
||||
warnings.extend(setting_notes);
|
||||
write_or_return(path, &out.bytes, json!({ "pages": plan.order.len(), "format": "postscript", "warnings": warnings }))
|
||||
}
|
||||
|
||||
@@ -49,6 +49,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
set
|
||||
),
|
||||
cmd!(
|
||||
"select.storedPath",
|
||||
"Select Path",
|
||||
[],
|
||||
None,
|
||||
"{id: object id, fingerprint: text (\"v1:\" and 16 lowercase hex digits)} the Actions panel's Select Path step: select only the path or compound path `id` when its fingerprint (a hash of its kind, fill rule and anchors, its members' for a compound path) is still `fingerprint` and it can be selected: not hidden or locked, nor in a hidden or locked layer or group, a template layer or on a locked artboard, and inside the isolated object in isolation mode. A missing object, another kind of object, a path whose kind, fill rule or anchors changed, or one that cannot be selected is an error, and the selection stays as it was → {count, ids} (the resulting selection)",
|
||||
has_doc,
|
||||
stored_path
|
||||
),
|
||||
cmd!(
|
||||
"select.add",
|
||||
"Add to Selection",
|
||||
@@ -449,6 +458,150 @@ fn selection_result(s: &Session) -> Result<Value> {
|
||||
Ok(json!({ "count": selection.len(), "ids": selection.objects }))
|
||||
}
|
||||
|
||||
/// The prefix of every fingerprint [`path_fingerprint`] gives: the version of its hash.
|
||||
const FINGERPRINT_PREFIX: &str = "v1:";
|
||||
|
||||
/// FNV-1a (64 bits) over the bytes written to it, starting from the offset basis.
|
||||
pub(crate) struct Fnv(pub(crate) u64);
|
||||
|
||||
impl Default for Fnv {
|
||||
fn default() -> Self {
|
||||
Self(0xcbf2_9ce4_8422_2325)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for Fnv {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.0 = buf.iter().fold(self.0, |h, b| (h ^ u64::from(*b)).wrapping_mul(0x0100_0000_01b3));
|
||||
Ok(buf.len())
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The fingerprint of the path or compound path `n` that `select.storedPath` compares: FNV-1a (64
|
||||
/// bits) over a kind byte, the fill rule and the anchors of the path, or of each member of the
|
||||
/// compound path in order, as JSON, written as [`FINGERPRINT_PREFIX`] and 16 lowercase hex digits.
|
||||
/// None for any object [`is_storable_kind`] does not take.
|
||||
pub fn path_fingerprint(n: &Node) -> Option<String> {
|
||||
fn feed(h: &mut Fnv, n: &Node) -> Option<()> {
|
||||
use std::io::Write;
|
||||
match &n.kind {
|
||||
NodeKind::Path { path, rule, .. } => {
|
||||
h.write_all(b"P").ok()?;
|
||||
serde_json::to_writer(&mut *h, rule).ok()?;
|
||||
serde_json::to_writer(&mut *h, path).ok()
|
||||
}
|
||||
NodeKind::Compound { children, rule } => {
|
||||
h.write_all(b"C").ok()?;
|
||||
serde_json::to_writer(&mut *h, rule).ok()?;
|
||||
h.write_all(&(children.len() as u64).to_le_bytes()).ok()?;
|
||||
children.iter().try_for_each(|c| feed(h, c))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
if !is_storable_kind(n) {
|
||||
return None;
|
||||
}
|
||||
let mut h = Fnv::default();
|
||||
feed(&mut h, n)?;
|
||||
Some(format!("{FINGERPRINT_PREFIX}{:016x}", h.0))
|
||||
}
|
||||
|
||||
/// Whether `n` is an object a Select Path step can store: a path that is not a guide, or a compound
|
||||
/// path whose members all are.
|
||||
fn is_storable_kind(n: &Node) -> bool {
|
||||
let path = |n: &Node| matches!(n.kind, NodeKind::Path { guide: false, .. });
|
||||
match &n.kind {
|
||||
NodeKind::Compound { children, .. } => children.iter().all(|c| path(c)),
|
||||
_ => path(n),
|
||||
}
|
||||
}
|
||||
|
||||
/// The `id` and `fingerprint` of `select.storedPath`'s params `p`, or why they are not ones: an
|
||||
/// object with only a non-negative whole `id` and a `fingerprint` that [`path_fingerprint`] could give.
|
||||
pub fn stored_path_params(p: &Value) -> std::result::Result<(NodeId, &str), String> {
|
||||
let o = p.as_object().ok_or("`params` must be an object")?;
|
||||
if let Some(key) = o.keys().find(|k| !matches!(k.as_str(), "id" | "fingerprint")) {
|
||||
let keys = "id, fingerprint";
|
||||
return Err(format!("unknown parameter `{key}`; the command takes: {keys}"));
|
||||
}
|
||||
let value = o.get("id").unwrap_or(&Value::Null);
|
||||
let id = value.as_u64().map(NodeId).ok_or_else(|| format!("invalid object id {value}: expected a non-negative integer"))?;
|
||||
let fingerprint = o.get("fingerprint").and_then(Value::as_str).unwrap_or_default();
|
||||
let hex = fingerprint.strip_prefix(FINGERPRINT_PREFIX).unwrap_or_default();
|
||||
if hex.len() != 16 || !hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
|
||||
return Err("`fingerprint` must be \"v1:\" and 16 lowercase hex digits".into());
|
||||
}
|
||||
Ok((id, fingerprint))
|
||||
}
|
||||
|
||||
/// Why the object `id` of `st` cannot be the path of a Select Path step: it is not there, is not a
|
||||
/// path (not a guide) or compound path ([`is_storable_kind`]), is a member of a compound path, cannot
|
||||
/// be selected ([`is_selectable`]), or is on a locked artboard, in isolation mode too.
|
||||
fn storable_path(st: &DocState, id: NodeId) -> std::result::Result<(), String> {
|
||||
let value = id.0;
|
||||
let n = st.doc.node(id).ok_or_else(|| format!("no such object id {value}"))?;
|
||||
let in_compound = st.doc.parent_of(id).and_then(|p| st.doc.node(p)).is_some_and(|p| matches!(p.kind, NodeKind::Compound { .. }));
|
||||
if !is_storable_kind(n) || in_compound {
|
||||
return Err(WHOLE_PATH.into());
|
||||
}
|
||||
// In isolation mode `is_selectable` does not look at the locked artboards, so they are checked here too.
|
||||
let locks = st.doc.locked_artboards();
|
||||
if !is_selectable(&st.doc, st.isolation, &locks, id) || locks.holds_id(id) {
|
||||
return Err("the path is hidden or locked, or cannot be selected here".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What [`storable_selection`] asks for when the selection is not a whole path or compound path.
|
||||
const WHOLE_PATH: &str = "select one whole path or compound path";
|
||||
|
||||
/// The one object selected in `st` when it can be the path of a Select Path step: a whole path or
|
||||
/// compound path that can be selected ([`storable_path`]). An error for no selection, several
|
||||
/// objects, a path with ruler guides, anchors or segments of a path alone, a group, type or any other object, a guide, or a
|
||||
/// member of a compound path. It does not hash the path, so menus can ask it every frame.
|
||||
pub fn storable_selection(st: &DocState) -> std::result::Result<NodeId, String> {
|
||||
let sel = &st.selection;
|
||||
let id = match sel.objects.as_slice() {
|
||||
[] => return Err("select a path".into()),
|
||||
[id] if sel.guides.is_empty() => *id,
|
||||
_ => return Err("select only one path".into()),
|
||||
};
|
||||
if sel.anchors.get(&id).is_some_and(|a| !a.is_empty()) || sel.segments.get(&id).is_some_and(|s| !s.is_empty()) {
|
||||
return Err(WHOLE_PATH.into());
|
||||
}
|
||||
storable_path(st, id)?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// The params of a `select.storedPath` step for the path selected in `st` ([`storable_selection`]):
|
||||
/// `{id, fingerprint}`.
|
||||
pub fn stored_path_step(st: &DocState) -> std::result::Result<Value, String> {
|
||||
let id = storable_selection(st)?;
|
||||
let fingerprint = st.doc.node(id).and_then(path_fingerprint).ok_or(WHOLE_PATH)?;
|
||||
Ok(json!({ "id": id.0, "fingerprint": fingerprint }))
|
||||
}
|
||||
|
||||
/// `select.storedPath {id, fingerprint}`: selects only the path `id` when it is still the path the
|
||||
/// step stored ([`path_fingerprint`]) and can be selected ([`storable_path`]); else an error, and
|
||||
/// the selection stays as it was.
|
||||
fn stored_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "select.storedPath";
|
||||
let (id, fingerprint) = stored_path_params(p).map_err(|e| bad(C, e))?;
|
||||
let st = s.doc()?;
|
||||
let value = id.0;
|
||||
let n = st.doc.node(id).ok_or_else(|| bad(C, format!("no such object id {value}")))?;
|
||||
if path_fingerprint(n).as_deref() != Some(fingerprint) {
|
||||
return Err(bad(C, format!("object {value} is not the stored path: it changed, or the document is another one")));
|
||||
}
|
||||
storable_path(st, id).map_err(|e| bad(C, e))?;
|
||||
s.select(|_, sel| sel.set([id]))?;
|
||||
selection_result(s)
|
||||
}
|
||||
|
||||
fn key(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = p.get("id").map(|value| checked_id(s, value, "select.key")).transpose()?;
|
||||
let guide = match p.get("guide").filter(|v| !v.is_null()) {
|
||||
|
||||
@@ -84,7 +84,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Select All Unused",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{} → {names: [the swatches nothing uses, in panel order]}: a colour is used when a paint links to it or an unlinked paint, gradient stop or mesh point has its colour; a gradient when a gradient links to it or has its stops; a pattern when it fills or strokes anything (art, symbols, pattern tiles and graphic styles count). None is never listed",
|
||||
"{} → {names: [the swatches nothing uses, in panel order]}: a colour is used when a paint links to it or an unlinked paint, gradient stop or mesh point has its colour; a gradient when a gradient links to it or has its stops; a pattern when it fills or strokes anything (art, symbols, pattern tiles and graphic styles count); a color, spot or process, when an image placed from a DCS file that the art shows has a spot plate of its name. None is never listed",
|
||||
has_doc,
|
||||
swatch_unused
|
||||
),
|
||||
@@ -988,8 +988,28 @@ fn swatch_unused(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let colors: HashSet<ColorKey> = colors.iter().filter_map(color_key).collect();
|
||||
let patterns: HashSet<&String> = patterns.iter().collect();
|
||||
let gradients: HashSet<GradientKey> = gradients.iter().filter_map(gradient_key).collect();
|
||||
// The spot inks the images placed from DCS files have plates for: their swatches print them.
|
||||
// Only images the art shows count: a deleted one's pixels stay for undo.
|
||||
let mut shown: HashSet<&str> = HashSet::new();
|
||||
for root in d
|
||||
.layers
|
||||
.iter()
|
||||
.chain(d.symbols.iter().map(|s| &s.art))
|
||||
.chain(d.graph_designs.iter().map(|g| &g.art))
|
||||
.chain(d.patterns.iter().flat_map(|p| &p.art))
|
||||
{
|
||||
vectorcraft_doc::swatches::walk_with_masks(root, &mut |n| {
|
||||
if let vectorcraft_doc::NodeKind::Image(im) = &n.kind {
|
||||
shown.insert(im.key.as_str());
|
||||
}
|
||||
});
|
||||
}
|
||||
let plate_inks: HashSet<String> =
|
||||
shown.iter().filter_map(|k| d.images.get(*k)).flat_map(vectorcraft_doc::ImageBlob::spot_plates).map(|(name, _)| name).collect();
|
||||
let used = |w: &Swatch| {
|
||||
gradient_links.contains(&w.name)
|
||||
// A spot swatch prints the plate; a process one gives the color it prints as process.
|
||||
|| (w.paint.color().is_some() && plate_inks.contains(&w.name))
|
||||
|| match w.tint_of() {
|
||||
Some((base, tint)) => float_key(tint).is_some_and(|t| link_tints.contains(&(base, t))),
|
||||
None => link_names.contains(w.name.as_str()),
|
||||
|
||||
@@ -32,6 +32,7 @@ pub use cmd::distortcmds::perspective_click;
|
||||
pub use cmd::params::{check_keys, check_params};
|
||||
pub use cmd::pathops::auto_simplify_level;
|
||||
pub use cmd::rasterfx::{export_pdf, flatten_raster_effects};
|
||||
pub use cmd::select::{path_fingerprint, storable_selection, stored_path_params, stored_path_step};
|
||||
pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
|
||||
pub use cmd::{EyedropperOptions, PathfinderOptions};
|
||||
pub use journal::Journal;
|
||||
|
||||
@@ -184,7 +184,9 @@ impl Session {
|
||||
if changed.is_empty() {
|
||||
return Ok(none());
|
||||
}
|
||||
let ids: Vec<u64> = changed.iter().flat_map(|(_, ids)| ids.iter().copied()).collect();
|
||||
// Each once: a DCS file and its plate files list the same objects.
|
||||
let mut seen = HashSet::new();
|
||||
let ids: Vec<u64> = changed.iter().flat_map(|(_, ids)| ids.iter().copied()).filter(|id| seen.insert(*id)).collect();
|
||||
let mode = if update { "automatically" } else { self.prefs.update_links.as_str() };
|
||||
match mode {
|
||||
"automatically" => {
|
||||
|
||||
@@ -44,6 +44,7 @@ mod containers;
|
||||
mod css;
|
||||
mod cut;
|
||||
mod dashalign;
|
||||
mod dcs;
|
||||
mod distort;
|
||||
mod docsetup;
|
||||
mod document_move;
|
||||
@@ -175,6 +176,7 @@ mod smartguides;
|
||||
mod smooth_path;
|
||||
mod spelling;
|
||||
mod store;
|
||||
mod stored_path;
|
||||
mod strokegeom;
|
||||
mod strokegradient;
|
||||
mod strokereach;
|
||||
|
||||
@@ -462,6 +462,49 @@ fn a_colorized_missing_link_placeholder_is_not_flattened() {
|
||||
assert_eq!(colorize(&s, id), Some(Paint::solid(Color::rgb8(255, 0, 0))), "the image keeps its paint");
|
||||
}
|
||||
|
||||
/// A fill color on a colorized image that shows the missing-link placeholder recolors the image,
|
||||
/// as the Fill proxy shows its colorize paint, and adds no fill item.
|
||||
#[test]
|
||||
fn a_fill_color_recolors_a_colorized_missing_link_placeholder() {
|
||||
let mut s = session();
|
||||
let bytes = std::sync::Arc::new(vectorcraft_doc::links::missing_placeholder().unwrap());
|
||||
let id = image(&mut s, "pic", ImageBlob { mime: "image/png".into(), bytes: bytes.clone(), proxy: Some(bytes) });
|
||||
s.edit("Colorize", |d, _| {
|
||||
d.node_mut(id).unwrap().extras_mut().colorize = Some(Box::new(Paint::solid(Color::rgb8(255, 0, 0))));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let appearance = s.doc().unwrap().doc.node(id).unwrap().appearance.clone();
|
||||
s.execute("paint.setFill", &json!({"color": "#0000ff", "ids": [id.0]})).unwrap();
|
||||
assert_eq!(colorize(&s, id).and_then(|p| p.color()).map(|c| c.to_hex()), Some("#0000ff".into()));
|
||||
assert_eq!(s.doc().unwrap().doc.node(id).unwrap().appearance, appearance, "no fill item");
|
||||
assert_eq!(s.proxy_paints().0.color().map(|c| c.to_hex()), Some("#0000ff".into()));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(colorize(&s, id), Some(Paint::solid(Color::rgb8(255, 0, 0))));
|
||||
}
|
||||
|
||||
/// The pixels behind the placeholder may have an alpha channel, so a missing-link image colorized
|
||||
/// with a spot color takes another spot color, black or white, and refuses a process color.
|
||||
#[test]
|
||||
fn a_colorized_missing_link_placeholder_in_a_spot_color_refuses_a_process_color() {
|
||||
let mut s = session();
|
||||
let bytes = std::sync::Arc::new(vectorcraft_doc::links::missing_placeholder().unwrap());
|
||||
let id = image(&mut s, "pic", ImageBlob { mime: "image/png".into(), bytes: bytes.clone(), proxy: Some(bytes) });
|
||||
s.execute("swatch.new", &json!({"name": "Ink 2", "color": {"c": 100, "m": 0, "y": 0, "k": 0}, "spot": true})).unwrap();
|
||||
s.edit("Colorize", |d, _| {
|
||||
let ink = Paint::Solid { color: Color::rgb8(255, 0, 0), swatch: Some("Ink".into()), tint: 1.0 };
|
||||
d.node_mut(id).unwrap().extras_mut().colorize = Some(Box::new(ink));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let before = colorize(&s, id);
|
||||
let e = s.execute("paint.setFill", &json!({"color": "#0000ff", "ids": [id.0]})).unwrap_err().to_string();
|
||||
assert!(e.contains("a process color can't colorize a grayscale image with transparency"), "{e}");
|
||||
assert_eq!(colorize(&s, id), before);
|
||||
s.execute("paint.setFill", &json!({"swatch": "Ink 2", "ids": [id.0]})).unwrap();
|
||||
assert!(matches!(colorize(&s, id), Some(Paint::Solid { swatch: Some(w), .. }) if w == "Ink 2"), "{:?}", colorize(&s, id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattened_colorized_image_shows_the_art_under_white_pixels() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -256,6 +256,122 @@ fn an_images_stroke_with_outline_object_draws_exports_and_expands_along_its_fram
|
||||
assert!(kids[0].appearance.fill_paint().is_none());
|
||||
}
|
||||
|
||||
/// A 100×100 PNG, transparent but for an opaque red disc of radius 30 in its middle.
|
||||
fn disc_png() -> Vec<u8> {
|
||||
let mut png = vec![];
|
||||
image::RgbaImage::from_fn(100, 100, |x, y| {
|
||||
let r = (f64::from(x) + 0.5 - 50.0).hypot(f64::from(y) + 0.5 - 50.0);
|
||||
image::Rgba([255, 0, 0, if r <= 30.0 { 255 } else { 0 }])
|
||||
})
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
png
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_images_stroke_with_outline_object_follows_its_opaque_area() {
|
||||
let mut s = session();
|
||||
// The frame runs from 50 to 150, the disc from 70 to 130.
|
||||
let r = run(&mut s, "file.place", json!({"name": "disc.png", "dataBase64": vectorcraft_format::base64_encode(&disc_png()), "at": [100, 100]}));
|
||||
let im = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
run(&mut s, "appearance.addStroke", json!({}));
|
||||
run(&mut s, "appearance.setItem", json!({"index": 0, "weight": 8, "color": "#000000"}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "path.outlineObject", "item": 0}));
|
||||
// Under the image, the stroke's outer 4 pt shows around the disc, from 66 to 70; nothing runs
|
||||
// along the frame.
|
||||
let shade = |s: &Session, x: u32, y: u32| match pixel(s, x, y) {
|
||||
p if p == RED => "red",
|
||||
[r, g, b, _] if r.max(g).max(b) < 40 => "black",
|
||||
[r, g, b, _] if r.min(g).min(b) > 215 => "white",
|
||||
p => panic!("({x}, {y}) is {p:?}"),
|
||||
};
|
||||
assert_eq!([shade(&s, 64, 100), shade(&s, 67, 100), shade(&s, 100, 100), shade(&s, 100, 132)], ["white", "black", "red", "black"]);
|
||||
assert_eq!([shade(&s, 49, 100), shade(&s, 52, 100)], ["white", "white"]);
|
||||
// SVG and PDF hold the disc's outline stroked 8 pt.
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
let disc = |b: vectorcraft_geom::Rect| [b.x0 - 70.0, b.y0 - 70.0, b.x1 - 130.0, b.y1 - 130.0].iter().all(|d| d.abs() < 0.5);
|
||||
let svg = vectorcraft_svg::export(&doc, &Default::default());
|
||||
assert!(svg.contains("<image") && svg.contains("stroke-width=\"8\""), "{svg}");
|
||||
let back = vectorcraft_pdf::import(&vectorcraft_pdf::export(&doc, &Default::default()).unwrap()).unwrap();
|
||||
let mut outlines = 0;
|
||||
for l in &back.layers {
|
||||
l.walk(&mut |n| {
|
||||
let stroked = (n.appearance.stroke_width() - 8.0).abs() < 0.01;
|
||||
outlines += usize::from(matches!(n.kind, NodeKind::Path { .. }) && stroked && n.geometric_bounds().is_some_and(disc));
|
||||
});
|
||||
}
|
||||
assert_eq!(outlines, 1);
|
||||
// Expand Appearance leaves the disc's outline below the image.
|
||||
run(&mut s, "effect.expandAppearance", json!({}));
|
||||
let n = node(&s, im);
|
||||
let kids = n.children().unwrap();
|
||||
assert!(kids.len() == 2 && matches!(kids[1].kind, NodeKind::Image(_)), "{n:?}");
|
||||
assert!(disc(kids[0].geometric_bounds().unwrap()), "{:?}", kids[0].geometric_bounds());
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
// An Offset Path of 3 pt after Outline Object moves the stroke 3 pt out: it shows from 63 to 70.
|
||||
run(&mut s, "effect.apply", json!({"effect": "path.offsetPath", "params": {"offset": 3}, "item": 0}));
|
||||
assert_eq!([shade(&s, 61, 100), shade(&s, 64, 100)], ["white", "black"]);
|
||||
// Relinked to an opaque image, the stroke follows the frame 3 pt out: it shows from 43 to 50.
|
||||
let dir = vectorcraft_testkit::temp_dir("outline-object-relink");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("opaque.png");
|
||||
let mut png = vec![];
|
||||
image::RgbaImage::from_pixel(100, 100, image::Rgba([255, 0, 0, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
std::fs::write(&path, png).unwrap();
|
||||
run(&mut s, "links.relink", json!({"ids": [im.0], "path": path}));
|
||||
assert_eq!([shade(&s, 41, 100), shade(&s, 45, 100), shade(&s, 64, 100)], ["white", "black", "red"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_images_outline_object_stroke_exports_selects_and_measures_along_its_opaque_area() {
|
||||
let mut s = session();
|
||||
// The frame runs from 50 to 150, the disc from 70 to 130.
|
||||
let r = run(&mut s, "file.place", json!({"name": "disc.png", "dataBase64": vectorcraft_format::base64_encode(&disc_png()), "at": [100, 100]}));
|
||||
let im = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
run(&mut s, "appearance.addStroke", json!({}));
|
||||
run(&mut s, "appearance.setItem", json!({"index": 0, "weight": 20, "color": "#000000"}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "path.outlineObject", "item": 0}));
|
||||
// EPS and the PDF-compatible .ai draw the disc's outline stroked 20 pt, 20 pt inside the
|
||||
// image's frame on each side (EPS moves the art to its bounding box).
|
||||
let disc_outlines = |d: &vectorcraft_doc::Document| {
|
||||
let mut frame = None;
|
||||
d.walk(|c| {
|
||||
if matches!(c.kind, NodeKind::Image(_)) {
|
||||
frame = c.geometric_bounds();
|
||||
}
|
||||
});
|
||||
let f = frame.unwrap();
|
||||
let mut n = 0;
|
||||
d.walk(|c| {
|
||||
let disc = |b: vectorcraft_geom::Rect| {
|
||||
[b.x0 - f.x0 - 20.0, b.y0 - f.y0 - 20.0, f.x1 - b.x1 - 20.0, f.y1 - b.y1 - 20.0].iter().all(|d| d.abs() < 0.6)
|
||||
};
|
||||
let stroked = (c.appearance.stroke_width() - 20.0).abs() < 0.01;
|
||||
n += usize::from(matches!(c.kind, NodeKind::Path { .. }) && stroked && c.geometric_bounds().is_some_and(disc));
|
||||
});
|
||||
n
|
||||
};
|
||||
let saved = |s: &mut Session, p: Value| vectorcraft_format::base64_decode(run(s, "file.saveCopy", p)["dataBase64"].as_str().unwrap()).unwrap();
|
||||
let eps = saved(&mut s, json!({"format": "eps", "previewFormat": "none", "thumbnails": false}));
|
||||
assert_eq!(disc_outlines(&vectorcraft_eps::import_with(&eps, false).unwrap().document), 1);
|
||||
let ai = saved(&mut s, json!({"format": "ai"}));
|
||||
assert_eq!(disc_outlines(&crate::cmd::fileio::page_document(&ai, 0, &Default::default()).unwrap().0), 1);
|
||||
// An Offset Path of 30 pt after Outline Object: the stroke runs round a circle of radius 60,
|
||||
// from 30 to 50 on the left, where along the frame it would run from 10 to 30.
|
||||
run(&mut s, "effect.apply", json!({"effect": "path.offsetPath", "params": {"offset": 30}, "item": 0}));
|
||||
assert_eq!((pixel(&s, 35, 100), pixel(&s, 15, 100)), ([0, 0, 0, 255], [255; 4]));
|
||||
run(&mut s, "select.none", json!({}));
|
||||
assert_eq!(click(&mut s, 35.0, 100.0), [im]);
|
||||
run(&mut s, "select.none", json!({}));
|
||||
assert!(click(&mut s, 15.0, 100.0).is_empty());
|
||||
// Use Preview Bounds measures the stroke round the circle.
|
||||
s.prefs.use_preview_bounds = true;
|
||||
let b = s.transform_bounds(&[im]).unwrap();
|
||||
assert!([b.x0 - 30.0, b.y0 - 30.0, b.x1 - 170.0, b.y1 - 170.0].iter().all(|d| d.abs() < 1.0), "{b:?}");
|
||||
}
|
||||
|
||||
/// Click with the Selection tool at (x, y) on a 100% view: what is selected afterwards.
|
||||
fn click(s: &mut Session, x: f64, y: f64) -> Vec<NodeId> {
|
||||
let v = ViewInfo::default();
|
||||
|
||||
@@ -0,0 +1,697 @@
|
||||
//! DCS files placed with Link (`file.place`): one linked image of their plates, their spot plates
|
||||
//! as spot swatches, clipped by their composite's clipping path, never opened or embedded; and
|
||||
//! the Links panel's Show DCS Transparency Interactions (`links.list` with `dcsTransparency`).
|
||||
//! The files are made from scratch as http://www.telegraphics.net/doc/DCS.txt describes them.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
|
||||
use crate::*;
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// A fresh scratch folder for one test.
|
||||
fn temp_dir(name: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("vc-dcs-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// A plate program: one 2 × 2 grayscale image (`gray`, row by row) on a 20 × 10 pt page.
|
||||
fn plate_eps(gray: [u8; 4]) -> String {
|
||||
let hex: String = gray.iter().map(|g| format!("{g:02x}")).collect();
|
||||
format!(
|
||||
"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EndComments\n20 10 scale\n2 2 8 [2 0 0 -2 0 2] <{hex}> image\nshowpage\n%%Trailer\n%%EOF\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// A single-file DCS 2.0 file of `plates` (name, plate program), with the header lines `extra`; its
|
||||
/// composite is drawn through a clipping path, the left half of the page.
|
||||
fn single_file(plates: &[(&str, String)], extra: &str) -> Vec<u8> {
|
||||
let header = |offsets: &[usize]| {
|
||||
let mut s = String::from("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n");
|
||||
s.push_str(extra);
|
||||
for ((name, eps), at) in plates.iter().zip(offsets) {
|
||||
s.push_str(&format!("%%PlateFile: ({name}) EPS #{at:010} {:010}\n", eps.len()));
|
||||
}
|
||||
s.push_str("%%EndComments\ngsave newpath 0 0 moveto 10 0 lineto 10 10 lineto 0 10 lineto closepath clip newpath\n");
|
||||
s.push_str("20 10 scale 2 2 8 [2 0 0 -2 0 2] <808080808080808080808080> false 3 colorimage grestore showpage\n%%EOF\n");
|
||||
s
|
||||
};
|
||||
let main = header(&vec![0; plates.len()]).len();
|
||||
let mut offsets = vec![];
|
||||
let mut at = main;
|
||||
for (_, eps) in plates {
|
||||
offsets.push(at);
|
||||
at += eps.len();
|
||||
}
|
||||
let mut out = header(&offsets).into_bytes();
|
||||
for (_, eps) in plates {
|
||||
out.extend(eps.as_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Cyan solid on the top row, PANTONE 185 C at 50% on the left column.
|
||||
fn two_plates() -> Vec<u8> {
|
||||
single_file(
|
||||
&[("Cyan", plate_eps([0, 0, 255, 255])), ("PANTONE 185 C", plate_eps([127, 255, 127, 255]))],
|
||||
"%%DocumentCustomColors: (PANTONE 185 C)\n%%CMYKCustomColor: 0 .91 .76 0 (PANTONE 185 C)\n",
|
||||
)
|
||||
}
|
||||
|
||||
fn write(dir: &std::path::Path, name: &str, bytes: &[u8]) -> String {
|
||||
let p = dir.join(name);
|
||||
std::fs::write(&p, bytes).unwrap();
|
||||
p.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
/// The image inside placed object `id` (itself, or the image of its clipping group).
|
||||
fn image_of(s: &Session, id: &Value) -> Node {
|
||||
let n = s.doc().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().clone();
|
||||
let mut found = None;
|
||||
n.walk(&mut |c: &Node| {
|
||||
if found.is_none() && matches!(c.kind, NodeKind::Image(_)) {
|
||||
found = Some(c.clone());
|
||||
}
|
||||
});
|
||||
found.expect("an image")
|
||||
}
|
||||
|
||||
fn error(s: &mut Session, cmd: &str, p: Value) -> String {
|
||||
s.execute(cmd, &p).unwrap_err().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dcs_file_places_as_one_linked_image_of_its_plates() {
|
||||
let dir = temp_dir("place");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
|
||||
assert_eq!(r["linked"], true);
|
||||
assert_eq!(r["warnings"], json!([]));
|
||||
let id = &r["ids"][0];
|
||||
let placed = s.doc().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().clone();
|
||||
let NodeKind::Group { children, clip: true } = &placed.kind else { panic!("a clipping group: {:?}", placed.kind) };
|
||||
assert!(matches!(children[0].kind, NodeKind::Path { clipping: true, .. }), "the composite's clipping path");
|
||||
assert_eq!(children[0].geometric_bounds().map(|b| b.width().round()), Some(10.0));
|
||||
assert_eq!(r["width"].as_f64().map(f64::round), Some(10.0), "clipped to the left half of its 20 pt page");
|
||||
let image = image_of(&s, id);
|
||||
let NodeKind::Image(im) = &image.kind else { not_an_image() };
|
||||
assert!(im.link.as_ref().is_some_and(|l| l.path.ends_with("pic.eps")));
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let plates = doc.images[&im.key].separated().expect("its plates are kept");
|
||||
assert_eq!(plates.plates.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Cyan", "PANTONE 185 C"]);
|
||||
assert_eq!(plates.plates[1].data, [128, 0, 128, 0]);
|
||||
let sw = doc.swatch("PANTONE 185 C").expect("a spot swatch for the spot plate");
|
||||
assert!(sw.spot);
|
||||
assert_eq!(sw.paint, Paint::solid(Color::cmyk(0.0, 0.91, 0.76, 0.0)));
|
||||
assert!(!doc.swatches_iter().any(|w| w.spot && w.name == "Cyan"), "process plates add no spot swatch");
|
||||
let list = s.execute("links.list", &json!({})).unwrap();
|
||||
assert_eq!(list["links"][0]["format"], "DCS");
|
||||
assert_eq!(list["links"][0]["status"], "ok");
|
||||
let info = s.execute("file.place.info", &json!({ "path": path })).unwrap();
|
||||
assert_eq!(info["plates"], json!(["Cyan", "PANTONE 185 C"]));
|
||||
assert_eq!(info["colorMode"], "CMYK");
|
||||
assert_eq!(info["pixelWidth"], 2);
|
||||
// One undo step takes the swatch away too.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.swatch("PANTONE 185 C").is_none());
|
||||
}
|
||||
|
||||
fn not_an_image() -> ! {
|
||||
panic!("an image")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dcs_file_is_never_opened_or_embedded() {
|
||||
let dir = temp_dir("refused");
|
||||
let bytes = two_plates();
|
||||
let path = write(&dir, "pic.eps", &bytes);
|
||||
let mut s = session();
|
||||
let only = "DCS files can only be placed linked";
|
||||
assert!(error(&mut s, "file.place", json!({ "path": path, "link": false })).contains(only));
|
||||
let data = json!({ "name": "pic.eps", "dataBase64": vectorcraft_format::base64_encode(&bytes) });
|
||||
assert!(error(&mut s, "file.place", data).contains(only), "bytes alone can't be linked");
|
||||
assert!(error(&mut s, "document.open", json!({ "path": path })).contains(only));
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let image = image_of(&s, &r["ids"][0]);
|
||||
let e = error(&mut s, "links.embed", json!({ "ids": [image.id.0] }));
|
||||
assert!(e.contains(only), "{e}");
|
||||
let NodeKind::Image(im) = &image_of(&s, &r["ids"][0]).kind else { not_an_image() };
|
||||
assert!(im.link.is_some(), "still linked");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_swatch_of_the_same_name_is_kept_with_a_warning() {
|
||||
let dir = temp_dir("swatch");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
s.execute("swatch.new", &json!({"name": "PANTONE 185 C", "color": {"c": 0.0, "m": 1.0, "y": 1.0, "k": 0.0}, "spot": true})).unwrap();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
assert_eq!(r["warnings"], json!(["the document's swatch `PANTONE 185 C` is kept: it is a different color"]));
|
||||
let sw = s.doc().unwrap().doc.swatch("PANTONE 185 C").unwrap().clone();
|
||||
assert_eq!(sw.paint.color(), Some(Color::cmyk(0.0, 1.0, 1.0, 0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plate_files_of_their_own_are_read_from_the_dcs_files_folder() {
|
||||
let dir = temp_dir("files");
|
||||
write(&dir, "pic.C", plate_eps([0, 255, 255, 255]).as_bytes());
|
||||
write(&dir, "pic.K", plate_eps([255, 255, 255, 0]).as_bytes());
|
||||
let main = "%!PS-Adobe-2.0 EPSF-1.2\n%%BoundingBox: 0 0 20 10\n%%CyanPlate: OldDisk:Scans:pic.C\n%%BlackPlate: pic.K\n%%EndComments\nshowpage\n";
|
||||
let path = write(&dir, "pic.eps", main.as_bytes());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let image = image_of(&s, &r["ids"][0]);
|
||||
assert!(matches!(image.kind, NodeKind::Image(_)), "no clipping path without a composite");
|
||||
let NodeKind::Image(im) = &image.kind else { not_an_image() };
|
||||
let plates = s.doc().unwrap().doc.images[&im.key].separated().unwrap();
|
||||
assert_eq!(plates.plates[0].data, [255, 0, 0, 0], "the plate file found by its name in the folder");
|
||||
assert_eq!(plates.plates[1].data, [0, 0, 0, 255]);
|
||||
std::fs::remove_file(dir.join("pic.K")).unwrap();
|
||||
let e = error(&mut s, "file.place", json!({ "path": path }));
|
||||
assert!(e.contains("the Black plate can't be read") && e.contains("`pic.K` is not in the DCS file's folder"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_link_reads_the_plates_again() {
|
||||
let dir = temp_dir("update");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let id = image_of(&s, &r["ids"][0]).id;
|
||||
let changed = single_file(&[("Cyan", plate_eps([0, 0, 0, 0])), ("Varnish", plate_eps([255; 4]))], "%%CMYKCustomColor: 0 0 .2 0 (Varnish)\n");
|
||||
std::fs::write(&path, &changed).unwrap();
|
||||
let u = s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
assert_eq!(u["updated"], json!([id.0]));
|
||||
let NodeKind::Image(im) = &s.doc().unwrap().doc.node(id).unwrap().kind else { not_an_image() };
|
||||
let plates = s.doc().unwrap().doc.images[&im.key].separated().unwrap();
|
||||
assert_eq!(plates.plates.len(), 2);
|
||||
assert_eq!(plates.plates[0].data, [255; 4]);
|
||||
let sw = s.doc().unwrap().doc.swatch("Varnish").expect("a spot swatch for the plate the file gained");
|
||||
assert!(sw.spot);
|
||||
assert_eq!(sw.paint, Paint::solid(Color::cmyk(0.0, 0.0, 0.2, 0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_link_takes_the_files_new_clipping_path() {
|
||||
let dir = temp_dir("update-clip");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
|
||||
let group = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
let id = image_of(&s, &r["ids"][0]).id;
|
||||
let clip_width = |s: &Session| {
|
||||
let NodeKind::Group { children, .. } = &s.doc().unwrap().doc.node(group).unwrap().kind else { panic!("a clipping group") };
|
||||
children[0].geometric_bounds().map(|b| b.width())
|
||||
};
|
||||
let before = clip_width(&s).unwrap();
|
||||
// The same file, its composite now drawn through the left quarter of the page (same length,
|
||||
// so the plates stay where the header says).
|
||||
let text = String::from_utf8(two_plates()).unwrap().replacen("10 0 lineto 10 10 lineto", " 5 0 lineto 5 10 lineto", 1);
|
||||
std::fs::write(&path, text.as_bytes()).unwrap();
|
||||
let u = s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
assert_eq!(u["updated"], json!([id.0]));
|
||||
let after = clip_width(&s).unwrap();
|
||||
assert!((after - before / 2.0).abs() < 1e-6, "the clipping path follows the file: {before} → {after}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_link_clips_an_image_whose_file_gained_a_clipping_path() {
|
||||
let dir = temp_dir("update-gained-clip");
|
||||
// The composite drawn without its clipping path (same length).
|
||||
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
|
||||
let path = write(&dir, "pic.eps", unclipped.as_bytes());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let id = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
assert!(matches!(s.doc().unwrap().doc.node(id).unwrap().kind, NodeKind::Image(_)), "no clipping path, no group");
|
||||
std::fs::write(&path, two_plates()).unwrap();
|
||||
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let group = doc.node(doc.parent_of(id).expect("a parent")).unwrap();
|
||||
let NodeKind::Group { children, clip: true } = &group.kind else { panic!("a clipping group: {:?}", group.kind) };
|
||||
assert_eq!(children[0].geometric_bounds().map(|b| b.width().round()), Some(10.0), "the left half of the page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_changed_plate_file_marks_the_link_modified() {
|
||||
let dir = temp_dir("plate-changed");
|
||||
write(&dir, "pic.C", plate_eps([0, 255, 255, 255]).as_bytes());
|
||||
let main = "%!PS-Adobe-2.0 EPSF-1.2\n%%BoundingBox: 0 0 20 10\n%%CyanPlate: pic.C\n%%EndComments\nshowpage\n";
|
||||
let path = write(&dir, "pic.eps", main.as_bytes());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let id = image_of(&s, &r["ids"][0]).id;
|
||||
assert_eq!(s.execute("links.list", &json!({})).unwrap()["links"][0]["status"], "ok");
|
||||
// Only the plate file changes (a size of its own, so the change shows whatever the clock).
|
||||
write(&dir, "pic.C", format!("{}% more\n", plate_eps([255, 255, 255, 0])).as_bytes());
|
||||
assert_eq!(s.execute("links.list", &json!({})).unwrap()["links"][0]["status"], "modified");
|
||||
let u = s.execute("links.update", &json!({})).unwrap();
|
||||
assert_eq!(u["updated"], json!([id.0]));
|
||||
let NodeKind::Image(im) = &s.doc().unwrap().doc.node(id).unwrap().kind else { not_an_image() };
|
||||
assert_eq!(s.doc().unwrap().doc.images[&im.key].separated().unwrap().plates[0].data, [0, 0, 0, 255]);
|
||||
assert_eq!(s.execute("links.list", &json!({})).unwrap()["links"][0]["status"], "ok");
|
||||
std::fs::remove_file(dir.join("pic.C")).unwrap();
|
||||
assert_eq!(s.execute("links.list", &json!({})).unwrap()["links"][0]["status"], "modified", "a plate file that is gone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_link_watch_reads_the_link_again_when_only_a_plate_file_changes() {
|
||||
let dir = temp_dir("plate-watch");
|
||||
write(&dir, "pic.C", plate_eps([0, 255, 255, 255]).as_bytes());
|
||||
let main = "%!PS-Adobe-2.0 EPSF-1.2\n%%BoundingBox: 0 0 20 10\n%%CyanPlate: pic.C\n%%EndComments\nshowpage\n";
|
||||
let path = write(&dir, "pic.eps", main.as_bytes());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let id = image_of(&s, &r["ids"][0]).id;
|
||||
let files: Vec<String> = crate::link_watch::linked_files(&s.doc().unwrap().doc).into_iter().map(|(p, _)| p).collect();
|
||||
assert!(files.iter().any(|p| p.ends_with("pic.C")), "the plate file is watched too: {files:?}");
|
||||
// The first look only remembers the files.
|
||||
assert_eq!(s.execute("links.updateChanged", &json!({})).unwrap()["updated"], json!([]));
|
||||
write(&dir, "pic.C", format!("{}% more\n", plate_eps([255, 255, 255, 0])).as_bytes());
|
||||
assert_eq!(s.execute("links.updateChanged", &json!({})).unwrap()["updated"], json!([id.0]));
|
||||
let NodeKind::Image(im) = &s.doc().unwrap().doc.node(id).unwrap().kind else { not_an_image() };
|
||||
assert_eq!(s.doc().unwrap().doc.images[&im.key].separated().unwrap().plates[0].data, [0, 0, 0, 255]);
|
||||
}
|
||||
|
||||
/// The width of the clipping path of the clip group holding the image in the art of symbol
|
||||
/// `name`; `None` when no clip group holds it.
|
||||
fn symbol_clip_width(s: &Session, name: &str) -> Option<f64> {
|
||||
fn find(n: &Node) -> Option<Option<f64>> {
|
||||
if let NodeKind::Group { children, clip: true } = &n.kind
|
||||
&& let [c, im] = &children[..]
|
||||
&& matches!(im.kind, NodeKind::Image(_))
|
||||
{
|
||||
return Some(c.geometric_bounds().map(|b| b.width()));
|
||||
}
|
||||
if matches!(n.kind, NodeKind::Image(_)) {
|
||||
return Some(None);
|
||||
}
|
||||
n.children().and_then(|ch| ch.iter().find_map(|c| find(c)))
|
||||
}
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
find(&doc.symbols.iter().find(|w| w.name == name).expect("the symbol").art).expect("an image in the symbol")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_link_takes_the_files_clipping_path_into_symbols_too() {
|
||||
let dir = temp_dir("update-symbol-clip");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
|
||||
let id = image_of(&s, &r["ids"][0]).id;
|
||||
let other = s.execute("file.place", &json!({ "path": path, "at": [200, 100] })).unwrap();
|
||||
s.execute("symbol.new", &json!({ "name": "Pic", "ids": [other["ids"][0]] })).unwrap();
|
||||
let before = symbol_clip_width(&s, "Pic").expect("the file's clipping path");
|
||||
// The composite now drawn through the left quarter of the page (same length).
|
||||
let text = String::from_utf8(two_plates()).unwrap().replacen("10 0 lineto 10 10 lineto", " 5 0 lineto 5 10 lineto", 1);
|
||||
std::fs::write(&path, text.as_bytes()).unwrap();
|
||||
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
let after = symbol_clip_width(&s, "Pic").expect("still clipped");
|
||||
assert!((after - before / 2.0).abs() < 1e-6, "the symbol's clipping path follows the file: {before} → {after}");
|
||||
// The file without a clipping path (same length): the symbol's image is no longer clipped.
|
||||
let unclipped = text.replacen("closepath clip newpath", "closepath newpath ", 1);
|
||||
std::fs::write(&path, unclipped.as_bytes()).unwrap();
|
||||
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
assert_eq!(symbol_clip_width(&s, "Pic"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_that_lost_its_clipping_path_unclips_the_image() {
|
||||
let dir = temp_dir("update-lost-clip");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let id = image_of(&s, &r["ids"][0]).id;
|
||||
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
|
||||
std::fs::write(&path, unclipped.as_bytes()).unwrap();
|
||||
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let placed = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
assert_eq!(doc.node(placed).and_then(plain_group_of_image), Some((false, true)), "the placed group stays, without the clip");
|
||||
let parent = doc.parent_of(id).expect("a parent");
|
||||
assert!(!matches!(doc.node(parent).unwrap().kind, NodeKind::Group { clip: true, .. }), "the image is not clipped");
|
||||
// Relinked to a plain image: no clip group either.
|
||||
let png = dir.join("plain.png");
|
||||
image::RgbImage::from_pixel(2, 2, image::Rgb([9, 9, 9])).save(&png).unwrap();
|
||||
std::fs::write(&path, two_plates()).unwrap();
|
||||
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
assert!(
|
||||
s.doc().unwrap().doc.parent_of(id).is_some_and(|p| matches!(s.doc().unwrap().doc.node(p).unwrap().kind, NodeKind::Group { clip: true, .. }))
|
||||
);
|
||||
s.execute("links.relink", &json!({ "ids": [id.0], "path": png.to_string_lossy() })).unwrap();
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
assert!(
|
||||
!matches!(doc.node(doc.parent_of(id).unwrap()).unwrap().kind, NodeKind::Group { clip: true, .. }),
|
||||
"a plain image isn't clipped by the DCS file's path"
|
||||
);
|
||||
}
|
||||
|
||||
/// The kind of group `g` is, as (clip, the kinds of its children are just one image).
|
||||
fn plain_group_of_image(g: &Node) -> Option<(bool, bool)> {
|
||||
match &g.kind {
|
||||
NodeKind::Group { children, clip } => Some((*clip, matches!(&children[..], [c] if matches!(c.kind, NodeKind::Image(_))))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hidden_or_transparent_clip_group_loses_the_files_old_clipping_path() {
|
||||
let dir = temp_dir("lost-clip-kept-group");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let place = |s: &mut Session, x: f64| s.execute("file.place", &json!({ "path": path, "at": [x, 100] })).unwrap();
|
||||
let (faded, hidden, in_symbol) = (place(&mut s, 100.0), place(&mut s, 200.0), place(&mut s, 300.0));
|
||||
let (faded, hidden) = (NodeId(faded["ids"][0].as_u64().unwrap()), NodeId(hidden["ids"][0].as_u64().unwrap()));
|
||||
let image = image_of(&s, &json!(faded.0)).id;
|
||||
s.execute("transparency.set", &json!({"ids": [faded.0, in_symbol["ids"][0]], "opacity": 50})).unwrap();
|
||||
s.edit("hide", |d, _| {
|
||||
if let Some(n) = d.node_mut(hidden) {
|
||||
n.visible = false;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("symbol.new", &json!({ "name": "Pic", "ids": [in_symbol["ids"][0]] })).unwrap();
|
||||
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
|
||||
std::fs::write(&path, unclipped.as_bytes()).unwrap();
|
||||
let hidden_image = image_of(&s, &json!(hidden.0)).id;
|
||||
s.execute("links.update", &json!({ "ids": [image.0, hidden_image.0] })).unwrap();
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
for (g, why) in [(faded, "the transparent group"), (hidden, "the hidden group")] {
|
||||
let n = doc.node(g).unwrap_or_else(|| panic!("{why} stays"));
|
||||
assert_eq!(plain_group_of_image(n), Some((false, true)), "{why} holds the image alone, unclipped");
|
||||
}
|
||||
assert!((doc.node(faded).unwrap().opacity - 0.5).abs() < 1e-6, "its transparency kept");
|
||||
assert!(!doc.node(hidden).unwrap().visible, "still hidden");
|
||||
// In a symbol's art too.
|
||||
let art = &doc.symbols.iter().find(|w| w.name == "Pic").expect("the symbol").art;
|
||||
let mut found = None;
|
||||
art.walk(&mut |n: &Node| {
|
||||
if found.is_none() && n.children().is_some_and(|ch| ch.iter().any(|c| matches!(c.kind, NodeKind::Image(_)))) {
|
||||
found = plain_group_of_image(n);
|
||||
}
|
||||
});
|
||||
assert_eq!(found, Some((false, true)), "the symbol's transparent group holds the image alone, unclipped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clipping_mask_of_ones_own_stays_when_the_link_is_updated() {
|
||||
let dir = temp_dir("own-mask");
|
||||
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
|
||||
let path = write(&dir, "pic.eps", unclipped.as_bytes());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
|
||||
let id = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
let rect = s.execute("shape.rectangle", &json!({"x": 95, "y": 95, "width": 4, "height": 4})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("select.set", &json!({ "ids": [id.0, rect] })).unwrap();
|
||||
let g = NodeId(s.execute("object.clippingMask.make", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.parent_of(id), Some(g), "the clipping mask made in the document stays");
|
||||
let mask_width = |s: &Session| {
|
||||
let NodeKind::Group { children, .. } = &s.doc().unwrap().doc.node(g).unwrap().kind else { panic!("the mask's group") };
|
||||
children[0].geometric_bounds().map(|b| b.width().round())
|
||||
};
|
||||
assert_eq!(mask_width(&s), Some(4.0));
|
||||
// The file gains a clipping path: its clip group goes inside the mask, which keeps its path.
|
||||
std::fs::write(&path, two_plates()).unwrap();
|
||||
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let inner = doc.parent_of(id).expect("a parent");
|
||||
assert_ne!(inner, g);
|
||||
assert!(matches!(doc.node(inner).unwrap().kind, NodeKind::Group { clip: true, .. }), "the file's clip group");
|
||||
assert_eq!(doc.parent_of(inner), Some(g));
|
||||
assert_eq!(mask_width(&s), Some(4.0), "the mask's path is kept");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dcs_file_takes_the_place_parameters_an_eps_file_takes() {
|
||||
let dir = temp_dir("params");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
assert!(error(&mut s, "file.place", json!({ "path": path, "page": 0 })).contains("page"));
|
||||
assert!(error(&mut s, "file.place", json!({ "path": path, "crop": "trim" })).contains("crop"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clipped_placement_answers_its_image_for_the_links_commands() {
|
||||
let dir = temp_dir("image-id");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
assert_eq!(r["image"], json!(image_of(&s, &r["ids"][0]).id.0));
|
||||
let info = s.execute("links.info", &json!({ "id": r["image"] })).unwrap();
|
||||
assert_eq!(info["format"], "DCS");
|
||||
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
|
||||
let plain = write(&dir, "plain.eps", unclipped.as_bytes());
|
||||
assert!(s.execute("file.place", &json!({ "path": plain })).unwrap().get("image").is_none(), "an image placed alone is its own id");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_and_relink_warn_of_a_swatch_kept_for_a_new_spot_plate() {
|
||||
let dir = temp_dir("update-swatch");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let id = image_of(&s, &r["ids"][0]).id;
|
||||
s.execute("swatch.new", &json!({"name": "Varnish", "color": {"c": 1.0, "m": 0.0, "y": 0.0, "k": 0.0}})).unwrap();
|
||||
let changed = single_file(&[("Cyan", plate_eps([0, 0, 0, 0])), ("Varnish", plate_eps([255; 4]))], "%%CMYKCustomColor: 0 0 .2 0 (Varnish)\n");
|
||||
std::fs::write(&path, &changed).unwrap();
|
||||
let u = s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
let kept = "the document's swatch `Varnish` is kept: it is a process color, so the plate prints as process inks";
|
||||
assert_eq!(u["warnings"], json!([kept]));
|
||||
let other = write(&dir, "other.eps", &changed);
|
||||
let r = s.execute("links.relink", &json!({ "ids": [id.0], "path": other })).unwrap();
|
||||
assert_eq!(r["warnings"], json!([kept]));
|
||||
assert!(s.execute("links.update", &json!({ "ids": [id.0] })).unwrap().get("warnings").is_some(), "every read warns");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_deleted_dcs_image_leaves_its_spot_swatch_unused() {
|
||||
let dir = temp_dir("unused");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let unused = |s: &mut Session| s.execute("swatch.unused", &json!({})).unwrap()["names"].as_array().unwrap().iter().any(|n| n == "PANTONE 185 C");
|
||||
assert!(!unused(&mut s), "in use while the image shows it");
|
||||
s.execute("select.set", &json!({ "ids": [r["ids"][0]] })).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
assert!(unused(&mut s), "unused once the image is deleted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_process_swatch_giving_a_dcs_plate_its_color_is_used() {
|
||||
let dir = temp_dir("unused-process");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
s.execute("swatch.new", &json!({"name": "PANTONE 185 C", "color": {"c": 1.0, "m": 0.0, "y": 0.0, "k": 0.0}})).unwrap();
|
||||
let unused = |s: &mut Session| s.execute("swatch.unused", &json!({})).unwrap()["names"].as_array().unwrap().iter().any(|n| n == "PANTONE 185 C");
|
||||
assert!(unused(&mut s), "nothing uses it yet");
|
||||
s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
assert!(!unused(&mut s), "the plate of its name prints as its process inks");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_opened_clipping_path_is_not_the_files_and_stays() {
|
||||
let dir = temp_dir("opened-clip");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let group = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
let id = image_of(&s, &r["ids"][0]).id;
|
||||
// The clipping path opened, its anchors where they were.
|
||||
s.edit("open", |d, _| {
|
||||
let clip = d.node(group).and_then(|g| g.children()).map(|ch| ch[0].id).unwrap();
|
||||
if let Some(Node { kind: NodeKind::Path { path, .. }, .. }) = d.node_mut(clip) {
|
||||
path.subpaths[0].closed = false;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
|
||||
std::fs::write(&path, unclipped.as_bytes()).unwrap();
|
||||
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.parent_of(id), Some(group), "a clipping path edited in the document stays");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clipping_path_given_another_fill_rule_is_not_the_files_and_stays() {
|
||||
let dir = temp_dir("rule-clip");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let group = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
let id = image_of(&s, &r["ids"][0]).id;
|
||||
// The clipping path's fill rule changed, its anchors where they were.
|
||||
s.edit("rule", |d, _| {
|
||||
let clip = d.node(group).and_then(|g| g.children()).map(|ch| ch[0].id).unwrap();
|
||||
if let Some(Node { kind: NodeKind::Path { rule, .. }, .. }) = d.node_mut(clip) {
|
||||
*rule = vectorcraft_geom::FillRule::EvenOdd;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
|
||||
std::fs::write(&path, unclipped.as_bytes()).unwrap();
|
||||
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.parent_of(id), Some(group), "a clipping path whose fill rule was changed stays");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_even_odd_clipping_path_is_placed_and_matched_under_the_nonzero_rule() {
|
||||
let dir = temp_dir("eo-clip");
|
||||
// The same length, so the plates' offsets hold.
|
||||
let eo = String::from_utf8(two_plates()).unwrap().replacen("gsave newpath 0 0 moveto", "gsave 0 0 moveto ", 1);
|
||||
let eo = eo.replacen("closepath clip newpath", "closepath eoclip newpath", 1);
|
||||
assert_eq!(eo.len(), two_plates().len());
|
||||
let path = write(&dir, "pic.eps", eo.as_bytes());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let group = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
let id = image_of(&s, &r["ids"][0]).id;
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let clip = &doc.node(group).and_then(|g| g.children()).expect("the file's clip group")[0];
|
||||
assert!(matches!(clip.kind, NodeKind::Path { rule: vectorcraft_geom::FillRule::NonZero, clipping: true, .. }), "{:?}", clip.kind);
|
||||
// The file without its clipping path: the group's clipping path is the file's, and goes.
|
||||
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
|
||||
std::fs::write(&path, unclipped.as_bytes()).unwrap();
|
||||
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.node(group).and_then(plain_group_of_image), Some((false, true)), "the file's clipping path is gone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_updated_together_warn_when_they_add_one_ink_in_two_colors() {
|
||||
let dir = temp_dir("update-two-inks");
|
||||
let (a, b) = (write(&dir, "a.eps", &two_plates()), write(&dir, "b.eps", &two_plates()));
|
||||
let mut s = session();
|
||||
s.execute("file.place", &json!({ "path": a })).unwrap();
|
||||
s.execute("file.place", &json!({ "path": b })).unwrap();
|
||||
let varnish =
|
||||
|c: &str| single_file(&[("Cyan", plate_eps([0; 4])), ("Varnish", plate_eps([255; 4]))], &format!("%%CMYKCustomColor: {c} (Varnish)\n"));
|
||||
std::fs::write(&a, varnish("0 0 .2 0")).unwrap();
|
||||
std::fs::write(&b, varnish("1 0 0 0")).unwrap();
|
||||
let u = s.execute("links.update", &json!({})).unwrap();
|
||||
assert_eq!(u["updated"].as_array().map(Vec::len), Some(2), "{u}");
|
||||
assert_eq!(u["warnings"], json!(["the document's swatch `Varnish` is kept: it is a different color"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relink_with_copy_refuses_a_dcs_file_with_plate_files() {
|
||||
let dir = temp_dir("copy");
|
||||
write(&dir, "pic.C", plate_eps([0, 255, 255, 255]).as_bytes());
|
||||
let main = "%!PS-Adobe-2.0 EPSF-1.2\n%%BoundingBox: 0 0 20 10\n%%CyanPlate: pic.C\n%%EndComments\nshowpage\n";
|
||||
let path = write(&dir, "pic.eps", main.as_bytes());
|
||||
let docs = dir.join("docs");
|
||||
std::fs::create_dir_all(&docs).unwrap();
|
||||
let mut s = session();
|
||||
s.execute("document.save", &json!({ "path": docs.join("doc.vectorcraft").to_string_lossy() })).unwrap();
|
||||
let png = dir.join("plain.png");
|
||||
image::RgbImage::from_pixel(2, 2, image::Rgb([9, 9, 9])).save(&png).unwrap();
|
||||
let r = s.execute("file.place", &json!({ "path": png.to_string_lossy() })).unwrap();
|
||||
let id = r["ids"][0].clone();
|
||||
let e = error(&mut s, "links.relink", json!({ "ids": [id], "path": path, "copy": true }));
|
||||
assert!(e.contains("plate files"), "{e}");
|
||||
assert!(!docs.join("pic.eps").exists(), "nothing is copied");
|
||||
// A single-file DCS file has its plates inside it: it is copied.
|
||||
let single = write(&dir, "single.eps", &two_plates());
|
||||
s.execute("links.relink", &json!({ "ids": [id], "path": single, "copy": true })).unwrap();
|
||||
assert!(docs.join("single.eps").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_saved_document_reads_the_plates_again_and_prints_the_spot_ink() {
|
||||
let dir = temp_dir("saved");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let saved = dir.join("doc.vectorcraft").to_string_lossy().into_owned();
|
||||
let mut s = session();
|
||||
s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
let unused = s.execute("swatch.unused", &json!({})).unwrap();
|
||||
assert!(!unused["names"].as_array().unwrap().iter().any(|n| n == "PANTONE 185 C"), "the plate's swatch is in use: {unused}");
|
||||
s.execute("document.save", &json!({ "path": saved })).unwrap();
|
||||
let mut s = Session::new();
|
||||
s.execute("document.open", &json!({ "path": saved })).unwrap();
|
||||
let list = s.execute("links.list", &json!({})).unwrap();
|
||||
assert_eq!(list["links"][0]["format"], "DCS");
|
||||
assert_eq!(list["links"][0]["status"], "ok");
|
||||
let id = NodeId(list["links"][0]["id"].as_u64().unwrap());
|
||||
let NodeKind::Image(im) = &s.doc().unwrap().doc.node(id).unwrap().kind else { not_an_image() };
|
||||
let plates = s.doc().unwrap().doc.images[&im.key].separated().expect("the plates, read from the file again");
|
||||
assert_eq!(plates.plates[1].data, [128, 0, 128, 0]);
|
||||
let v = s.execute("print.preview", &json!({"settings": {"output": {"mode": "separations"}}})).unwrap();
|
||||
let inks: Vec<&str> = v["sheets"].as_array().unwrap().iter().filter_map(|p| p["ink"].as_str()).collect();
|
||||
assert_eq!(inks, ["Cyan", "Magenta", "Yellow", "Black", "PANTONE 185 C"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dcs_transparency_marks_transparent_and_overlapped_dcs_images() {
|
||||
let dir = temp_dir("transparency");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
|
||||
let image = image_of(&s, &r["ids"][0]).id;
|
||||
let marked = |s: &mut Session| s.execute("links.list", &json!({ "dcsTransparency": true })).unwrap()["links"][0]["dcsTransparency"] == true;
|
||||
assert!(!marked(&mut s), "nothing transparent");
|
||||
assert!(s.execute("links.list", &json!({})).unwrap()["links"][0].get("dcsTransparency").is_none());
|
||||
// A half-transparent square away from it, then over it.
|
||||
let far = s.execute("shape.rectangle", &json!({"x": 300, "y": 200, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("transparency.set", &json!({"ids": [far], "opacity": 50})).unwrap();
|
||||
assert!(!marked(&mut s), "a transparent object elsewhere");
|
||||
s.execute("shape.rectangle", &json!({"x": 90, "y": 90, "width": 20, "height": 20})).unwrap();
|
||||
let over = s.execute("shape.rectangle", &json!({"x": 92, "y": 92, "width": 4, "height": 4})).unwrap()["id"].as_u64().unwrap();
|
||||
assert!(!marked(&mut s), "opaque objects over it");
|
||||
s.execute("transparency.set", &json!({"ids": [over], "blend": "multiply"})).unwrap();
|
||||
assert!(marked(&mut s), "a blend mode over it");
|
||||
s.execute("transparency.set", &json!({"ids": [over], "blend": "normal"})).unwrap();
|
||||
s.execute("transparency.set", &json!({"ids": [image.0], "opacity": 80})).unwrap();
|
||||
assert!(marked(&mut s), "the image itself");
|
||||
assert!(error(&mut s, "links.list", json!({ "dcsTransparency": "yes" })).contains("dcsTransparency"), "a flag that isn't true or false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dcs_transparency_counts_a_transparent_dcs_image_over_another() {
|
||||
let dir = temp_dir("transparency-two");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
let a = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
|
||||
let b = s.execute("file.place", &json!({ "path": path, "at": [103, 100] })).unwrap();
|
||||
let (a, b) = (image_of(&s, &a["ids"][0]).id, image_of(&s, &b["ids"][0]).id);
|
||||
s.execute("transparency.set", &json!({"ids": [a.0], "opacity": 80})).unwrap();
|
||||
let list = s.execute("links.list", &json!({ "dcsTransparency": true })).unwrap();
|
||||
let marked = |id: NodeId| list["links"].as_array().unwrap().iter().any(|r| r["id"] == id.0 && r["dcsTransparency"] == true);
|
||||
assert!(marked(a), "the transparent image");
|
||||
assert!(marked(b), "the opaque image it overlaps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dcs_transparency_counts_characters_with_a_transparent_gradient() {
|
||||
use vectorcraft_color::{Gradient, GradientKind, GradientPaint, GradientStop};
|
||||
let dir = temp_dir("transparency-text");
|
||||
let path = write(&dir, "pic.eps", &two_plates());
|
||||
let mut s = session();
|
||||
s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
|
||||
let t = NodeId(s.execute("text.create", &json!({"x": 96, "y": 104, "text": "Hi", "size": 12})).unwrap()["id"].as_u64().unwrap());
|
||||
let marked = |s: &mut Session| s.execute("links.list", &json!({ "dcsTransparency": true })).unwrap()["links"][0]["dcsTransparency"] == true;
|
||||
assert!(!marked(&mut s), "opaque characters over it");
|
||||
let mut stops = vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))];
|
||||
stops[1].opacity = 0.5;
|
||||
let gradient = Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(GradientKind::Linear, stops))));
|
||||
s.edit("t", |d, _| {
|
||||
if let Some(Node { kind: NodeKind::Text(text), .. }) = d.node_mut(t) {
|
||||
text.runs[0].style.fill = gradient.clone();
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert!(marked(&mut s), "characters filled with a gradient that has a transparent stop");
|
||||
assert!(s.doc().unwrap().doc.node(t).unwrap().shows_transparency());
|
||||
}
|
||||
@@ -3278,6 +3278,14 @@ fn close_path_places_the_first_anchors_handles() {
|
||||
s.execute("path.close", &json!({"id": id.0, "in": [60.0, 140.0], "out": [120.0, 60.0]})).unwrap();
|
||||
let b = path(&s, id).subpaths[0].anchors[0];
|
||||
assert_eq!((b.h_in, b.h_out, b.kind), (Point::new(60.0, 140.0), Point::new(120.0, 60.0), AnchorKind::Corner));
|
||||
// `at` moves the anchor, its handles with it, before `in` is placed.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
refuses_with(&mut s, "path.close", &json!({"id": id.0, "at": "120, 90"}), "`at` must be [x, y]");
|
||||
s.execute("path.close", &json!({"id": id.0, "at": [120.0, 90.0], "in": [60.0, 140.0]})).unwrap();
|
||||
let b = path(&s, id).subpaths[0].anchors[0];
|
||||
assert_eq!((b.p, b.h_in, b.h_out), (Point::new(120.0, 90.0), Point::new(60.0, 140.0), Point::new(170.0, 90.0)), "{b:?}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(path(&s, id).subpaths[0].anchors[0], a, "one undo step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -254,6 +254,35 @@ fn type_in_a_missing_family_warns_as_pdf_export_does() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The warnings of `file.print` (PDF and PostScript) and `print.preview` list what the job
|
||||
/// leaves out of the document or prints differently first, then the notes on the options.
|
||||
#[test]
|
||||
fn what_a_print_loses_comes_before_the_notes_on_the_options() {
|
||||
let (mut s, _) = session();
|
||||
s.execute("text.create", &json!({"x": 10, "y": 100, "text": "Ab", "font": "Nonexistent Sans Probe"})).unwrap();
|
||||
let notes = |v: &Value| -> Vec<String> { v["warnings"].as_array().unwrap().iter().filter_map(Value::as_str).map(str::to_string).collect() };
|
||||
// A fixed flatness isn't written to a PDF: a note on the options, made before the type's.
|
||||
let fixed = json!({"settings": {"graphics": {"autoFlatness": false}}});
|
||||
for v in [print(&mut s, fixed.clone()).1, s.execute("print.preview", &fixed).unwrap()] {
|
||||
let n = notes(&v);
|
||||
assert!(n.iter().any(|w| w.contains("Nonexistent Sans Probe")), "{n:?}");
|
||||
assert!(n.last().is_some_and(|w| w.starts_with("a fixed flatness")), "{n:?}");
|
||||
}
|
||||
// PostScript doesn't apply the printer profile.
|
||||
let profile = json!({"format": "postscript", "settings": {"color": {"profile": vectorcraft_color::cms::SRGB}}});
|
||||
let n = notes(&s.execute("file.print", &profile).unwrap());
|
||||
assert!(n.iter().any(|w| w.contains("Nonexistent Sans Probe")), "{n:?}");
|
||||
assert!(n.last().is_some_and(|w| w.starts_with("the printer profile")), "{n:?}");
|
||||
// An artboard larger than Letter: copies per sheet that don't fit come after the art that
|
||||
// doesn't print.
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 700, "height": 900})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 700, "height": 900})).unwrap();
|
||||
let n = notes(&print(&mut s, json!({"settings": {"copiesPerSheet": true}})).1);
|
||||
assert!(n.iter().any(|w| w.contains("part of it doesn't print")), "{n:?}");
|
||||
assert!(n.last().is_some_and(|w| w.ends_with("it prints one copy per sheet")), "{n:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_that_does_not_fit_its_frame_is_named() {
|
||||
let (mut s, _) = session();
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
//! `select.storedPath`, the Actions panel's Select Path step: the step stores a path's id and a
|
||||
//! fingerprint of it, and playback selects that path only while it is still there unchanged and
|
||||
//! can be selected.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// A document with two rectangles and an ellipse, nothing selected → the session and their ids.
|
||||
fn doc() -> (Session, [u64; 3]) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let mut add = |shape: &str, x: u32| s.execute(shape, &json!({"x": x, "y": 10, "width": 40, "height": 30})).unwrap()["id"].as_u64().unwrap();
|
||||
let ids = [add("shape.rectangle", 10), add("shape.rectangle", 80), add("shape.ellipse", 150)];
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
(s, ids)
|
||||
}
|
||||
|
||||
fn select(s: &mut Session, ids: &[u64]) {
|
||||
s.execute("select.set", &json!({"ids": ids})).unwrap();
|
||||
}
|
||||
|
||||
fn selected(s: &Session) -> Vec<u64> {
|
||||
s.doc().unwrap().selection.objects.iter().map(|id| id.0).collect()
|
||||
}
|
||||
|
||||
/// The Select Path step's params for `id`, selected alone.
|
||||
fn step_for(s: &mut Session, id: u64) -> Value {
|
||||
select(s, &[id]);
|
||||
stored_path_step(s.doc().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hash_is_fnv_1a_64() {
|
||||
// The published FNV-1a 64-bit test values for "", "a" and "foobar".
|
||||
for (bytes, want) in [(&b""[..], 0xcbf2_9ce4_8422_2325_u64), (b"a", 0xaf63_dc4c_8601_ec8c), (b"foobar", 0x8594_4171_f739_67e8)] {
|
||||
let mut h = crate::cmd::select::Fnv::default();
|
||||
h.write_all(bytes).unwrap();
|
||||
assert_eq!(h.0, want, "{bytes:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_step_stores_the_id_and_a_fingerprint_and_plays_by_selecting_the_path() {
|
||||
let (mut s, [a, b, _]) = doc();
|
||||
let p = step_for(&mut s, a);
|
||||
assert_eq!(p["id"], json!(a));
|
||||
let fingerprint = p["fingerprint"].as_str().unwrap();
|
||||
assert!(fingerprint.starts_with("v1:") && fingerprint.len() == 19, "{fingerprint}");
|
||||
assert_eq!(step_for(&mut s, a), p, "the same path gives the same step");
|
||||
assert_ne!(step_for(&mut s, b)["fingerprint"], p["fingerprint"], "another path gives another fingerprint");
|
||||
select(&mut s, &[b]);
|
||||
let r = s.execute("select.storedPath", &p).unwrap();
|
||||
assert_eq!(r, json!({"count": 1, "ids": [a]}));
|
||||
assert_eq!(selected(&s), [a], "only the stored path is selected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_compound_path_is_one_step_and_a_path_in_a_group_is_one_too() {
|
||||
let (mut s, [a, b, c]) = doc();
|
||||
select(&mut s, &[a, b]);
|
||||
let compound = s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].as_u64().unwrap();
|
||||
let p = step_for(&mut s, compound);
|
||||
let member = s.doc().unwrap().doc.children(Some(vectorcraft_doc::NodeId(compound))).unwrap()[0].id.0;
|
||||
select(&mut s, &[member]);
|
||||
assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), "select one whole path or compound path", "a member alone is not stored");
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.execute("select.storedPath", &p).unwrap();
|
||||
assert_eq!(selected(&s), [compound]);
|
||||
|
||||
select(&mut s, &[c, compound]);
|
||||
let group = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
|
||||
assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), "select one whole path or compound path", "a group is not stored");
|
||||
let inner = step_for(&mut s, c);
|
||||
select(&mut s, &[group]);
|
||||
s.execute("select.storedPath", &inner).unwrap();
|
||||
assert_eq!(selected(&s), [c], "the path inside the group is selected alone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_one_whole_path_that_can_be_selected_is_stored() {
|
||||
let (mut s, [a, b, _]) = doc();
|
||||
let whole = "select one whole path or compound path";
|
||||
assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), "select a path");
|
||||
select(&mut s, &[a, b]);
|
||||
assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), "select only one path");
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 200})).unwrap();
|
||||
select(&mut s, &[a]);
|
||||
s.execute("guide.select", &json!({"indexes": [0], "toggle": true})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.guides.len(), 1, "the guide is selected with the path");
|
||||
assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), "select only one path", "a path with a guide");
|
||||
s.execute("guide.select", &json!({"indexes": [0], "toggle": true})).unwrap();
|
||||
assert_eq!(stored_path_step(s.doc().unwrap()).unwrap(), step_for(&mut s, a), "the path alone again");
|
||||
s.execute("select.anchors", &json!({"id": a, "anchors": [[0, 0]], "mode": "set"})).unwrap();
|
||||
assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), whole, "an anchor alone");
|
||||
let t = s.execute("text.create", &json!({"x": 10, "y": 200, "text": "Type"})).unwrap()["id"].as_u64().unwrap();
|
||||
select(&mut s, &[t]);
|
||||
assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), whole, "type");
|
||||
select(&mut s, &[a]);
|
||||
s.execute("object.lock", &json!({})).unwrap();
|
||||
let mut st = s.doc().unwrap().clone();
|
||||
st.selection.objects = vec![vectorcraft_doc::NodeId(a)];
|
||||
assert_eq!(stored_path_step(&st).unwrap_err(), "the path is hidden or locked, or cannot be selected here");
|
||||
let guide = {
|
||||
let mut d = (*st.doc).clone();
|
||||
if let Some(vectorcraft_doc::NodeKind::Path { guide, .. }) = d.node_mut(vectorcraft_doc::NodeId(a)).map(|n| &mut n.kind) {
|
||||
*guide = true;
|
||||
}
|
||||
d
|
||||
};
|
||||
assert!(path_fingerprint(guide.node(vectorcraft_doc::NodeId(a)).unwrap()).is_none(), "a guide is not stored");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_changed_missing_hidden_or_other_path_is_an_error_and_keeps_the_selection() {
|
||||
let (mut s, [a, b, c]) = doc();
|
||||
let p = step_for(&mut s, a);
|
||||
let fails = |s: &mut Session, p: &Value, want: &str| {
|
||||
select(s, &[c]);
|
||||
let e = s.execute("select.storedPath", p).unwrap_err().to_string();
|
||||
assert!(e.contains(want), "{e}");
|
||||
assert_eq!(selected(s), [c], "the selection stays as it was");
|
||||
};
|
||||
select(&mut s, &[a]);
|
||||
s.execute("object.move", &json!({"dx": 5, "dy": 0})).unwrap();
|
||||
fails(&mut s, &p, "is not the stored path");
|
||||
let moved = step_for(&mut s, a);
|
||||
s.execute("object.hide", &json!({})).unwrap();
|
||||
fails(&mut s, &moved, "hidden or locked");
|
||||
s.execute("object.showAll", &json!({})).unwrap();
|
||||
let shown = step_for(&mut s, b);
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
fails(&mut s, &shown, "no such object id");
|
||||
// Another document: the id names another path there, or nothing.
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 1, "y": 1, "width": 7, "height": 9})).unwrap();
|
||||
let c2 = s.execute("shape.ellipse", &json!({"x": 20, "y": 20, "width": 9, "height": 7})).unwrap()["id"].as_u64().unwrap();
|
||||
select(&mut s, &[c2]);
|
||||
assert!(s.execute("select.storedPath", &moved).is_err());
|
||||
assert_eq!(selected(&s), [c2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_on_a_locked_artboard_is_refused_in_isolation_mode_too() {
|
||||
let (mut s, [a, b, _]) = doc();
|
||||
select(&mut s, &[a, b]);
|
||||
let group = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
|
||||
let p = step_for(&mut s, a);
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.execute("object.isolate", &json!({"id": group})).unwrap();
|
||||
s.execute("artboard.new", &json!({"x": 0, "y": 0, "width": 300, "height": 200})).unwrap();
|
||||
s.execute("artboard.setProps", &json!({"index": 1, "locked": true})).unwrap();
|
||||
assert!(s.doc().unwrap().isolation.is_some(), "the group stays isolated");
|
||||
assert!(s.doc().unwrap().doc.on_locked_artboard(vectorcraft_doc::NodeId(a)), "the path is on the locked artboard");
|
||||
let mut st = s.doc().unwrap().clone();
|
||||
st.selection.objects = vec![vectorcraft_doc::NodeId(a)];
|
||||
assert_eq!(stored_path_step(&st).unwrap_err(), "the path is hidden or locked, or cannot be selected here", "capture");
|
||||
let before = selected(&s);
|
||||
let e = s.execute("select.storedPath", &p).unwrap_err().to_string();
|
||||
assert!(e.contains("hidden or locked"), "{e}");
|
||||
assert_eq!(selected(&s), before, "the selection stays as it was");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failing_step_in_a_batch_undoes_the_steps_before_it() {
|
||||
let (mut s, [a, _, c]) = doc();
|
||||
let p = step_for(&mut s, a);
|
||||
select(&mut s, &[a]);
|
||||
s.execute("object.move", &json!({"dx": 5, "dy": 0})).unwrap();
|
||||
select(&mut s, &[c]);
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let steps = [json!({"command": "object.move", "params": {"dx": 1, "dy": 0}}), json!({"command": "select.storedPath", "params": p})];
|
||||
assert!(s.execute("command.batch", &json!({"commands": steps})).is_err());
|
||||
assert!(s.doc().unwrap().doc == before, "the move before the step is undone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn params_are_an_id_and_a_v1_fingerprint() {
|
||||
let (mut s, [a, ..]) = doc();
|
||||
let p = step_for(&mut s, a);
|
||||
let f = p["fingerprint"].clone();
|
||||
for bad in [
|
||||
json!({"fingerprint": f}),
|
||||
json!({"id": -1, "fingerprint": f}),
|
||||
json!({"id": "1", "fingerprint": f}),
|
||||
json!({"id": a}),
|
||||
json!({"id": a, "fingerprint": 5}),
|
||||
json!({"id": a, "fingerprint": "v2:0123456789abcdef"}),
|
||||
json!({"id": a, "fingerprint": "v1:0123456789ABCDEF"}),
|
||||
json!({"id": a, "fingerprint": "v1:0123456789abcde"}),
|
||||
json!({"id": a, "fingerprint": f, "extra": 1}),
|
||||
json!([a]),
|
||||
] {
|
||||
assert!(stored_path_params(&bad).is_err(), "{bad}");
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
assert!(s.execute("select.storedPath", &bad).is_err(), "{bad}");
|
||||
assert!(selected(&s).is_empty(), "{bad}");
|
||||
}
|
||||
assert!(stored_path_params(&p).is_ok());
|
||||
}
|
||||
@@ -4423,7 +4423,7 @@ fn type_attributes_the_data_cant_hold_are_reported() {
|
||||
let back = read_back(&doc);
|
||||
assert!(matches!(back.layers[0].children().unwrap()[0].kind, NodeKind::Text(_)));
|
||||
// Any other attribute the text document doesn't hold is reported too: No Break, a paragraph's
|
||||
// left-to-right direction set on it, Optical Margin Alignment, a font version, a font that isn't
|
||||
// left-to-right direction set on it, Optical Margin Alignment, a font that isn't
|
||||
// installed whose name doesn't come back (a hyphen in its family), a Lab color,
|
||||
// small capitals at another size, right-to-left letters aligned to the start of a paragraph
|
||||
// without a direction of its own, tracking that isn't a whole number.
|
||||
@@ -4432,7 +4432,6 @@ fn type_attributes_the_data_cant_hold_are_reported() {
|
||||
|t| t.runs[0].style.tracking = 12.5,
|
||||
|t| t.para.direction = Some(vectorcraft_doc::ParaDirection::LeftToRight),
|
||||
|t| t.optical_margin = true,
|
||||
|t| t.runs[0].style.font_version = Some("2.0".into()),
|
||||
|t| t.runs[0].style.font_family = "my-missing font".into(),
|
||||
|t| t.runs[0].style.fill = Paint::solid(MINT),
|
||||
|t| t.runs[0].style.small_caps = Some(80.0),
|
||||
@@ -4455,11 +4454,130 @@ fn type_attributes_the_data_cant_hold_are_reported() {
|
||||
missing.runs[0].style.font_family = "Missing Sans Probe".into();
|
||||
missing.runs[0].style.font_style = "Bold Italic".into();
|
||||
assert!(with(missing).is_empty());
|
||||
// A version that isn't installed draws in the face the font resolves to, and comes back in it.
|
||||
let mut uninstalled = t.clone();
|
||||
uninstalled.runs[0].style.font_version = Some("2.0".into());
|
||||
assert!(with(uninstalled).is_empty());
|
||||
edit(&mut t);
|
||||
assert_eq!(with(t), [writer::TYPE_ATTRIBUTES]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs the copies of each font `copies` lists, the first of each loaded first.
|
||||
fn install_copies(copies: &[(bool, vectorcraft_text::test_fonts::CopyNames<'_>)]) {
|
||||
for (inter, names) in copies {
|
||||
let font = vectorcraft_text::test_fonts::named_copy(*inter, *names).unwrap();
|
||||
assert_eq!(vectorcraft_text::FontDb::global().add_font(font), 1, "{names:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A run's text, font family and font version, as [`fonts_saved`] reads them back.
|
||||
type SavedRun = (String, String, Option<String>);
|
||||
|
||||
/// A 200 × 100 document of point type whose runs are `runs` (text, family, version).
|
||||
fn runs_doc(runs: &[(&str, &str, Option<&str>)]) -> Document {
|
||||
use vectorcraft_doc::{CharStyle, TextObject, TextRun};
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let layer = doc.layers[0].id;
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::new(10.0, 70.0), "", style(18.0, Color::BLACK));
|
||||
t.runs = runs
|
||||
.iter()
|
||||
.map(|(text, family, version)| {
|
||||
let st = CharStyle { font_family: (*family).into(), font_version: version.map(Into::into), ..style(18.0, Color::BLACK) };
|
||||
TextRun::new(*text, st)
|
||||
})
|
||||
.collect();
|
||||
let n = text_node(&mut doc, t);
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
doc
|
||||
}
|
||||
|
||||
/// [`runs_doc`] saved and read back: the save's warnings, and the runs read back.
|
||||
fn fonts_saved(runs: &[(&str, &str, Option<&str>)]) -> (Vec<String>, Vec<SavedRun>) {
|
||||
let doc = runs_doc(runs);
|
||||
let warnings = export_ai_editing(&doc, &opts()).unwrap().warnings;
|
||||
let back = read_back(&doc);
|
||||
let NodeKind::Text(t) = &back.layers[0].children().unwrap()[0].kind else { panic!("type comes back as type") };
|
||||
(warnings, t.runs.iter().map(|r| (r.text.clone(), r.style.font_family.clone(), r.style.font_version.clone())).collect())
|
||||
}
|
||||
|
||||
/// How many fonts the font set of [`runs_doc`] names, the hidden characters' font aside.
|
||||
fn fonts_written(runs: &[(&str, &str, Option<&str>)]) -> usize {
|
||||
text_document(&program(&runs_doc(runs))).matches("/CoolTypeFont").count() - 1
|
||||
}
|
||||
|
||||
/// Type in a font installed beside a family spelled alike but for case and punctuation
|
||||
/// (`Sample/Ink`, PostScript name `SampleInk`, beside `SAMPLE INK`), or beside a copy whose
|
||||
/// PostScript name differs only in case (the type naming its version), is written under its
|
||||
/// PostScript name and read back in its own family and version, so nothing about its font is
|
||||
/// reported lost.
|
||||
#[test]
|
||||
fn type_in_a_font_beside_a_family_spelled_alike_keeps_its_font() {
|
||||
install_copies(&[
|
||||
(true, ("SAVETEST INK", "Regular", "Version 1.000", "SAVETESTINK-Regular")),
|
||||
(false, ("Savetest/Ink", "Regular", "Version 1.000", "SavetestInk")),
|
||||
(true, ("CASESAVE INK", "Regular", "Version 1.000", "CASESAVEINK")),
|
||||
(false, ("Casesave Ink", "Regular", "Version 2.000", "CasesaveInk")),
|
||||
]);
|
||||
let (warnings, back) = fonts_saved(&[("Kept", "Savetest/Ink", None)]);
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert_eq!(back, [("Kept".to_string(), "Savetest/Ink".to_string(), None)]);
|
||||
let (warnings, back) = fonts_saved(&[("Kept", "Casesave Ink", Some("Version 2.000"))]);
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert_eq!(back, [("Kept".to_string(), "Casesave Ink".to_string(), Some("Version 2.000".to_string()))]);
|
||||
}
|
||||
|
||||
/// Type in a static font's version that the family and style don't resolve to (two installed
|
||||
/// copies of one PostScript name, family and style) is written with that version (`/5`) and comes
|
||||
/// back in it, without a note; two runs in two versions are two fonts of the set.
|
||||
#[test]
|
||||
fn type_in_another_version_of_a_static_font_keeps_it() {
|
||||
install_copies(&[(true, ("Rttest Ink", "Regular", "1.0", "RttestInk-Regular")), (false, ("Rttest Ink", "Regular", "1.1", "RttestInk-Regular"))]);
|
||||
let (warnings, back) = fonts_saved(&[("Kept", "Rttest Ink", Some("1.1"))]);
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert_eq!(back, [("Kept".to_string(), "Rttest Ink".to_string(), Some("1.1".to_string()))]);
|
||||
let (warnings, back) = fonts_saved(&[("Old", "Rttest Ink", None), ("New", "Rttest Ink", Some("1.1"))]);
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert_eq!(back, [("Old".to_string(), "Rttest Ink".to_string(), None), ("New".to_string(), "Rttest Ink".to_string(), Some("1.1".to_string()))]);
|
||||
// A run naming the version its family and style resolve to anyway, or one that isn't
|
||||
// installed, is in the same face as a run naming none: one font of the set, and no note.
|
||||
let (warnings, back) = fonts_saved(&[("Same", "Rttest Ink", Some("1.0")), ("Gone", "Rttest Ink", Some("9.9")), ("Plain", "Rttest Ink", None)]);
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert!(back.iter().all(|(_, family, version)| family == "Rttest Ink" && version.is_none()), "{back:?}");
|
||||
assert_eq!(fonts_written(&[("Same", "Rttest Ink", Some("1.0")), ("Gone", "Rttest Ink", Some("9.9")), ("Plain", "Rttest Ink", None)]), 1);
|
||||
}
|
||||
|
||||
/// Type in the copy of a font that its family names, beside a copy of the same PostScript name in
|
||||
/// another family that the name alone gives first, is written with its version (`/5`), so it comes
|
||||
/// back in its own copy and family, without a note.
|
||||
#[test]
|
||||
fn type_in_a_copy_its_postscript_name_alone_doesnt_give_keeps_it() {
|
||||
install_copies(&[
|
||||
(true, ("RvdupsaveInk", "Regular", "2.0", "RvdupsaveInk-Regular")),
|
||||
(false, ("Rvdupsave Ink", "Regular", "1.0", "RvdupsaveInk-Regular")),
|
||||
]);
|
||||
let (warnings, back) = fonts_saved(&[("Kept", "Rvdupsave Ink", None)]);
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert_eq!(back.iter().map(|r| r.1.as_str()).collect::<Vec<_>>(), ["Rvdupsave Ink"]);
|
||||
let (warnings, back) = fonts_saved(&[("Kept", "RvdupsaveInk", None)]);
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert_eq!(back.iter().map(|r| r.1.as_str()).collect::<Vec<_>>(), ["RvdupsaveInk"]);
|
||||
}
|
||||
|
||||
/// A version string longer than the reader takes (256 bytes) isn't one it can pick a copy by: type
|
||||
/// in such a copy, which its name alone doesn't give back, is reported as losing its font.
|
||||
#[test]
|
||||
fn a_version_longer_than_the_reader_takes_is_reported() {
|
||||
let long = "9".repeat(300);
|
||||
install_copies(&[
|
||||
(true, ("Longtest Ink", "Regular", "1.0", "LongtestInk-Regular")),
|
||||
(false, ("Longtest Ink", "Regular", &long, "LongtestInk-Regular")),
|
||||
]);
|
||||
let (warnings, back) = fonts_saved(&[("Long", "Longtest Ink", Some(&long))]);
|
||||
assert_eq!(warnings, [writer::TYPE_ATTRIBUTES]);
|
||||
assert_eq!(back, [("Long".to_string(), "Longtest Ink".to_string(), None)]);
|
||||
}
|
||||
|
||||
/// On a large canvas, area type, threaded frames and type on a path stay type: the frames and
|
||||
/// lengths are written at the canvas's scale. Frames of a thread at two scales, and area type
|
||||
/// scaled by two factors, are written as outlines.
|
||||
|
||||
@@ -338,6 +338,8 @@ pub(in crate::ai_editing) fn loses(t: &TextObject) -> bool {
|
||||
CharStyle {
|
||||
font_family: s.font_family.clone(),
|
||||
font_style: s.font_style.clone(),
|
||||
// The font record's version (`/5`) names the face; whether it comes back is `font_kept`'s.
|
||||
font_version: s.font_version.clone(),
|
||||
size: s.size,
|
||||
leading: s.leading,
|
||||
tracking: s.tracking.round(),
|
||||
@@ -452,15 +454,17 @@ struct FontRecord {
|
||||
/// the font's order, in the axis's own units. Empty for a static font, which has none: without
|
||||
/// it, an app that opens the file takes a variable font's default instance (#6388).
|
||||
design: Vec<f32>,
|
||||
/// For a variable font, its version string (`/5`, name ID 5); empty for a static font.
|
||||
/// Its version string (`/5`, name ID 5): for a variable font, and for a static font whose
|
||||
/// PostScript name alone gives the reader another installed copy (two copies of one font, see
|
||||
/// the reader's `document_font`); else empty.
|
||||
version: String,
|
||||
}
|
||||
|
||||
/// The font the font set names for style `st`: an installed face's PostScript name, and for a
|
||||
/// variable font the setting of each of its axes (the named instance's, or the style's own
|
||||
/// Variable Font setting, clamped to the axis as layout clamps it) and its version; else its family
|
||||
/// and style as `family-style` (the family alone for Regular), the form the reader's
|
||||
/// `family_style` splits.
|
||||
/// The font the font set names for style `st`: an installed face's PostScript name, its version
|
||||
/// for a variable font or when the name alone gives the reader another installed copy, and for a
|
||||
/// variable font the setting of each of its axes (the named instance's, or the style's own Variable
|
||||
/// Font setting, clamped to the axis as layout clamps it); else its family and style as
|
||||
/// `family-style` (the family alone for Regular), the form the reader's `family_style` splits.
|
||||
fn font_name(st: &CharStyle) -> FontRecord {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
// The face of that family and style (the font database gives a fallback face for others).
|
||||
@@ -479,8 +483,11 @@ fn font_name(st: &CharStyle) -> FontRecord {
|
||||
set.map_or(own, |v| (v as f32).clamp(a.min.min(a.max), a.max.max(a.min)))
|
||||
})
|
||||
.collect();
|
||||
let version = if design.is_empty() { String::new() } else { face.version.clone() };
|
||||
FontRecord { name: face.postscript_name(), truetype: face.is_truetype(), design, version }
|
||||
let name = face.postscript_name();
|
||||
// The reader takes the face of a name alone as `postscript_face` finds it.
|
||||
let by_name = crate::import::postscript_face(&name, None).is_some_and(|f| f.id() == face.id());
|
||||
let version = if design.is_empty() && by_name { String::new() } else { face.version.clone() };
|
||||
FontRecord { name, truetype: face.is_truetype(), design, version }
|
||||
}
|
||||
None if st.font_style.is_empty() || st.font_style == "Regular" => record(st.font_family.clone()),
|
||||
None => record(format!("{}-{}", st.font_family, st.font_style.replace(' ', ""))),
|
||||
@@ -518,12 +525,19 @@ fn fixed(v: f32) -> i64 {
|
||||
if v.is_finite() { (f64::from(v) * 65536.0).round() as i64 } else { 0 }
|
||||
}
|
||||
|
||||
/// Does the font of style `st` come back as itself: the family and style the reader makes of the
|
||||
/// name it is written under (as its `char_style` does)?
|
||||
/// Does the font of style `st` come back as itself: are the family and style the reader makes of
|
||||
/// the record it is written as (its name, and its version as the reader takes it, as its
|
||||
/// `char_style` does) the style's own, and the face they and the version read back resolve to the
|
||||
/// face of the style? A version that isn't installed draws in the face the font resolves to anyway.
|
||||
fn font_kept(st: &CharStyle) -> bool {
|
||||
let (family, style) = crate::family_style(&font_name(st).name);
|
||||
let family = vectorcraft_text::FontDb::global().find_family(&family).unwrap_or(family);
|
||||
family == st.font_family && style == st.font_style
|
||||
let record = font_name(st);
|
||||
let version = crate::import::record_version(&record.version);
|
||||
let (family, style, read) = crate::import::document_font(&record.name, version);
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let face = |family: &str, style: &str, version: Option<&str>| db.face_version(family, style, version).map(|f| f.id());
|
||||
family == st.font_family
|
||||
&& style == st.font_style
|
||||
&& face(&family, &style, read.as_deref()) == face(&st.font_family, &st.font_style, st.font_version.as_deref())
|
||||
}
|
||||
|
||||
/// Whether `c` is a strong right-to-left letter (`Some(true)`: Hebrew, Arabic, Syriac, Thaana, NKo
|
||||
@@ -1022,13 +1036,13 @@ impl TextDoc {
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let mut font = vec![(0, V::Str(f.name.clone())), (2, V::Int(i64::from(f.truetype)))];
|
||||
// A variable font's place in its design space and its version (#6388); a static
|
||||
// font has neither.
|
||||
// A variable font's place in its design space (#6388), and the version of a
|
||||
// variable font or of a static one its name alone doesn't give back.
|
||||
if !f.design.is_empty() {
|
||||
font.push((4, V::List(f.design.iter().map(|v| V::Int(fixed(*v))).collect())));
|
||||
if !f.version.is_empty() {
|
||||
font.push((5, V::Str(f.version.clone())));
|
||||
}
|
||||
}
|
||||
if !f.version.is_empty() {
|
||||
font.push((5, V::Str(f.version.clone())));
|
||||
}
|
||||
dict(vec![(99, V::Name("CoolTypeFont")), (0, dict(font))])
|
||||
})
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
//! DCS files (Desktop Color Separation): an EPS file whose header comments point at one EPS
|
||||
//! program per printing plate. A DCS 2.0 file names each plate with `%%PlateFile: (name) EPS
|
||||
//! #offset size`, the plate inside the file `offset` bytes from its start, or `%%PlateFile: (name)
|
||||
//! EPS Local file`, the plate in a file of its own; a DCS 1.0 file names the four process plates'
|
||||
//! files with `%%CyanPlate:`, `%%MagentaPlate:`, `%%YellowPlate:` and `%%BlackPlate:`. Spot inks
|
||||
//! take their process equivalents from `%%CMYKCustomColor:` (else `%%RGBCustomColor:`). The comments
|
||||
//! are described at http://www.telegraphics.net/doc/DCS.txt.
|
||||
//!
|
||||
//! [`read`] finds those comments; [`place`] reads every plate's image (its ink amounts: a dark
|
||||
//! plate pixel is ink) into one [`Separated`] image, with the clipping path the main file's
|
||||
//! composite is drawn through.
|
||||
|
||||
use vectorcraft_color::cms::PROCESS_PLATES;
|
||||
use vectorcraft_doc::separated::{InkPlate, MAX_NAME, MAX_PLATES, MAX_SAMPLES, Separated};
|
||||
use vectorcraft_doc::{Document, Node, NodeKind};
|
||||
use vectorcraft_geom::{Affine, Rect};
|
||||
|
||||
/// Where a plate's program is.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PlateAt {
|
||||
/// In the DCS file itself: `len` bytes from byte `offset` of the file.
|
||||
Here { offset: usize, len: usize },
|
||||
/// In a file of its own, as the DCS file names it (a path or a file name).
|
||||
File(String),
|
||||
}
|
||||
|
||||
/// One plate a DCS file names.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PlateRef {
|
||||
/// "Cyan", "Magenta", "Yellow", "Black" or a spot ink's name.
|
||||
pub name: String,
|
||||
pub at: PlateAt,
|
||||
}
|
||||
|
||||
/// The DCS comments of a file.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Dcs {
|
||||
/// 1 or 2.
|
||||
pub version: u8,
|
||||
pub plates: Vec<PlateRef>,
|
||||
/// Spot inks' process equivalents (C, M, Y, K, 0..1), by name.
|
||||
pub equivalents: Vec<(String, [f32; 4])>,
|
||||
}
|
||||
|
||||
impl Dcs {
|
||||
/// The names of the plate files kept apart from the DCS file.
|
||||
pub fn plate_files(&self) -> impl Iterator<Item = &str> {
|
||||
self.plates.iter().filter_map(|p| match &p.at {
|
||||
PlateAt::File(f) => Some(f.as_str()),
|
||||
PlateAt::Here { .. } => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The DCS comments of EPS file `bytes` (behind its binary header, if it has one); `None` when it
|
||||
/// names no plate.
|
||||
pub fn read(bytes: &[u8]) -> Option<Dcs> {
|
||||
let (ps, _) = crate::sections(bytes)?;
|
||||
if !ps.starts_with(b"%!") {
|
||||
return None;
|
||||
}
|
||||
// The plates of the DCS 2.0 comments and of the DCS 1.0 ones.
|
||||
let mut plates: Vec<PlateRef> = vec![];
|
||||
let mut old: Vec<PlateRef> = vec![];
|
||||
let mut equivalents: Vec<(String, [f32; 4])> = vec![];
|
||||
let mut rgb: Vec<(String, [f32; 4])> = vec![];
|
||||
// The comment a `%%+` line continues.
|
||||
let mut last = String::new();
|
||||
for raw in ps.split(|b| matches!(b, b'\n' | b'\r')).filter(|l| !l.is_empty()).take(MAX_HEADER_LINES) {
|
||||
let line = header_line(raw);
|
||||
let line = line.trim_end();
|
||||
if line.starts_with("%%EndComments") || !line.starts_with('%') {
|
||||
break;
|
||||
}
|
||||
let (key, value) = match line.strip_prefix("%%+") {
|
||||
Some(v) => (last.clone(), v.trim()),
|
||||
None => match line.split_once(':') {
|
||||
Some((k, v)) => (k.to_string(), v.trim()),
|
||||
None => (line.to_string(), ""),
|
||||
},
|
||||
};
|
||||
match key.as_str() {
|
||||
"%%PlateFile" => {
|
||||
if let Some(p) = plate_file(value)
|
||||
&& !plates.iter().any(|q| q.name == p.name)
|
||||
{
|
||||
plates.push(p);
|
||||
}
|
||||
}
|
||||
"%%CyanPlate" | "%%MagentaPlate" | "%%YellowPlate" | "%%BlackPlate" => {
|
||||
let name = key.trim_start_matches("%%").trim_end_matches("Plate").to_string();
|
||||
if !value.is_empty() && !old.iter().any(|q| q.name == name) {
|
||||
old.push(PlateRef { name, at: PlateAt::File(value.to_string()) });
|
||||
}
|
||||
}
|
||||
"%%CMYKCustomColor" => equivalents.extend(custom_colors(value, false)),
|
||||
"%%RGBCustomColor" => rgb.extend(custom_colors(value, true)),
|
||||
_ => {}
|
||||
}
|
||||
last = key;
|
||||
}
|
||||
// A file is one version or the other; the plate comments of DCS 2.0 win.
|
||||
let version = if plates.is_empty() {
|
||||
plates = old;
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
if plates.is_empty() {
|
||||
return None;
|
||||
}
|
||||
for (name, c) in rgb {
|
||||
if !equivalents.iter().any(|(n, _)| *n == name) {
|
||||
equivalents.push((name, c));
|
||||
}
|
||||
}
|
||||
Some(Dcs { version, plates, equivalents })
|
||||
}
|
||||
|
||||
/// Most header lines looked at for the DCS comments.
|
||||
const MAX_HEADER_LINES: usize = 10_000;
|
||||
|
||||
/// Header line `raw` as text: UTF-8 when it is, else Latin-1 (each byte one character), as the
|
||||
/// octal escapes of names are read.
|
||||
fn header_line(raw: &[u8]) -> std::borrow::Cow<'_, str> {
|
||||
match std::str::from_utf8(raw) {
|
||||
Ok(s) => std::borrow::Cow::Borrowed(s),
|
||||
Err(_) => std::borrow::Cow::Owned(raw.iter().copied().map(char::from).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
/// `s` without its first word and the whitespace around that word.
|
||||
fn after_word(s: &str) -> &str {
|
||||
s.trim_start().split_once(char::is_whitespace).map_or("", |(_, rest)| rest.trim_start())
|
||||
}
|
||||
|
||||
/// A `%%PlateFile:` comment's plate: `(name) EPS #offset size` or `(name) EPS Local file`; `None`
|
||||
/// for other locations, which only a print server reads.
|
||||
fn plate_file(v: &str) -> Option<PlateRef> {
|
||||
let (name, rest) = ps_string(v)?;
|
||||
let mut words = rest.split_whitespace();
|
||||
let _kind = words.next()?;
|
||||
let location = words.next()?;
|
||||
let name = canonical(name)?;
|
||||
if let Some(offset) = location.strip_prefix('#') {
|
||||
let len = words.next()?.parse().ok()?;
|
||||
return Some(PlateRef { name, at: PlateAt::Here { offset: offset.parse().ok()?, len } });
|
||||
}
|
||||
if !location.eq_ignore_ascii_case("Local") {
|
||||
return None;
|
||||
}
|
||||
// The file name as it is written, its spaces kept.
|
||||
let file = after_word(after_word(rest));
|
||||
(!file.is_empty()).then(|| PlateRef { name, at: PlateAt::File(file.to_string()) })
|
||||
}
|
||||
|
||||
/// A plate's name as it is kept: a process ink's in its usual case ("cyan" is "Cyan"); `None` when
|
||||
/// it is empty or longer than [`MAX_NAME`].
|
||||
fn canonical(name: String) -> Option<String> {
|
||||
if name.is_empty() || name.len() > MAX_NAME {
|
||||
return None;
|
||||
}
|
||||
Some(PROCESS_PLATES.iter().find(|p| p.eq_ignore_ascii_case(&name)).map_or(name, |p| p.to_string()))
|
||||
}
|
||||
|
||||
/// The custom colours of a `%%CMYKCustomColor:` (four numbers then a name, repeated) or, with
|
||||
/// `rgb`, a `%%RGBCustomColor:` comment (three numbers), the RGB ones as the process inks that
|
||||
/// take their light away (`1 − r`, `1 − g`, `1 − b`, no black). Names may be bare words.
|
||||
fn custom_colors(v: &str, rgb: bool) -> Vec<(String, [f32; 4])> {
|
||||
let mut out = vec![];
|
||||
let mut rest = v.trim();
|
||||
let n = if rgb { 3 } else { 4 };
|
||||
while !rest.is_empty() && out.len() < MAX_PLATES {
|
||||
let mut nums = [0.0f32; 4];
|
||||
for v in nums.iter_mut().take(n) {
|
||||
let Some((word, after)) = rest.split_once(char::is_whitespace) else { return out };
|
||||
let Some(x) = word.parse::<f32>().ok().filter(|x| (0.0..=1.0).contains(x)) else { return out };
|
||||
*v = x;
|
||||
rest = after.trim_start();
|
||||
}
|
||||
let (name, after) = match ps_string(rest) {
|
||||
Some((s, a)) => (s, a),
|
||||
None => {
|
||||
let (w, a) = rest.split_once(char::is_whitespace).unwrap_or((rest, ""));
|
||||
(w.to_string(), a)
|
||||
}
|
||||
};
|
||||
rest = after.trim_start();
|
||||
let cmyk = if rgb { [1.0 - nums[0], 1.0 - nums[1], 1.0 - nums[2], 0.0] } else { nums };
|
||||
if let Some(name) = canonical(name) {
|
||||
out.push((name, cmyk));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The PostScript string at the start of `v` (`(…)`, with nested parentheses and backslash
|
||||
/// escapes) and what follows it.
|
||||
fn ps_string(v: &str) -> Option<(String, &str)> {
|
||||
let body = v.trim_start().strip_prefix('(')?;
|
||||
let mut out = String::new();
|
||||
let mut depth = 0usize;
|
||||
let mut chars = body.char_indices().peekable();
|
||||
while let Some((i, c)) = chars.next() {
|
||||
match c {
|
||||
'\\' => match chars.next()?.1 {
|
||||
'n' => out.push('\n'),
|
||||
'r' => out.push('\r'),
|
||||
't' => out.push('\t'),
|
||||
d @ '0'..='7' => {
|
||||
let mut code = d.to_digit(8)?;
|
||||
for _ in 0..2 {
|
||||
match chars.peek().and_then(|(_, e)| e.to_digit(8)) {
|
||||
Some(e) => {
|
||||
chars.next();
|
||||
code = code * 8 + e;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
// Octal escapes name bytes; names are read as Latin-1.
|
||||
out.push(char::from_u32(code & 0xff)?);
|
||||
}
|
||||
e => out.push(e),
|
||||
},
|
||||
'(' => {
|
||||
depth += 1;
|
||||
out.push(c);
|
||||
}
|
||||
')' if depth == 0 => return Some((out, body.get(i + 1..)?)),
|
||||
')' => {
|
||||
depth -= 1;
|
||||
out.push(c);
|
||||
}
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The main part of DCS file `bytes`: its PostScript (behind its binary header, if it has one), up
|
||||
/// to the first plate it holds.
|
||||
pub fn main_program<'a>(bytes: &'a [u8], dcs: &Dcs) -> Option<&'a [u8]> {
|
||||
let (ps, _) = crate::sections(bytes)?;
|
||||
let start = ps.as_ptr() as usize - bytes.as_ptr() as usize;
|
||||
let first = dcs.plates.iter().filter_map(|p| match p.at {
|
||||
PlateAt::Here { offset, .. } if offset > start => Some(offset - start),
|
||||
_ => None,
|
||||
});
|
||||
Some(&ps[..first.min().unwrap_or(ps.len()).min(ps.len())])
|
||||
}
|
||||
|
||||
/// A DCS file read for placing.
|
||||
#[derive(Debug)]
|
||||
pub struct Placed {
|
||||
pub separated: Separated,
|
||||
/// The file's page (its bounding box) with its top-left corner at the origin.
|
||||
pub page: Rect,
|
||||
/// The image's pixels onto the page.
|
||||
pub xf: Affine,
|
||||
/// The clipping path the composite is drawn through, on the page.
|
||||
pub clip: Option<Node>,
|
||||
/// The resolution of the plates (pixels per inch), from how large they print.
|
||||
pub ppi: Option<(f64, f64)>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// One plate's image: its ink amounts, one byte a pixel, and where its pixels print on its page.
|
||||
struct PlateImage {
|
||||
width: u32,
|
||||
height: u32,
|
||||
data: Vec<u8>,
|
||||
xf: Affine,
|
||||
}
|
||||
|
||||
/// Read DCS file `bytes` (its comments `dcs`) for placing: every plate's image, the plates kept
|
||||
/// apart read with `file` (the name the file gives → the plate file's bytes). The plates are put on
|
||||
/// the pixel grid of the first plate's image, grown to hold every plate's image where it prints;
|
||||
/// another plate is resampled onto it. An error when the file names more than [`MAX_PLATES`]
|
||||
/// plates or a plate can't be read.
|
||||
pub fn place(bytes: &[u8], dcs: &Dcs, file: &mut dyn FnMut(&str) -> Result<Vec<u8>, String>) -> Result<Placed, String> {
|
||||
if dcs.plates.len() > MAX_PLATES {
|
||||
return Err(format!("it names {} plates, and at most {MAX_PLATES} are read", dcs.plates.len()));
|
||||
}
|
||||
let main = main_program(bytes, dcs).ok_or("the file's preview header points outside the file")?;
|
||||
let page = crate::page(main)?;
|
||||
let mut warnings = vec![];
|
||||
let (mut images, mut samples) = (vec![], 0u64);
|
||||
for p in &dcs.plates {
|
||||
let read = match &p.at {
|
||||
PlateAt::Here { offset, len } => {
|
||||
let eps = offset.checked_add(*len).and_then(|end| bytes.get(*offset..end));
|
||||
let eps = eps.ok_or_else(|| format!("the {} plate lies outside the file", p.name))?;
|
||||
plate_image(eps)
|
||||
}
|
||||
PlateAt::File(f) => file(f).and_then(|eps| plate_image(&eps)),
|
||||
};
|
||||
let img = read.map_err(|e| format!("the {} plate can't be read: {e}", p.name))?;
|
||||
// Checked before the other plates are read.
|
||||
samples += u64::from(img.width) * u64::from(img.height);
|
||||
if samples > MAX_SAMPLES {
|
||||
return Err(format!("its plates are too large: more than {MAX_SAMPLES} pixels together"));
|
||||
}
|
||||
images.push((p, img));
|
||||
}
|
||||
let Some((w, h, xf)) = grid(images.iter().map(|(_, img)| img)) else { return Err("the file names no plate".into()) };
|
||||
if !Separated::fits(w, h, images.len()) {
|
||||
return Err(format!("its plates are too large: {} plates of {w} × {h} pixels", images.len()));
|
||||
}
|
||||
let mut plates = vec![];
|
||||
for (p, img) in images {
|
||||
let name = p.name.clone();
|
||||
let aligned = img.xf.as_coeffs().iter().zip(xf.as_coeffs()).all(|(a, b)| (a - b).abs() < 1e-9);
|
||||
let data = if (img.width, img.height) == (w, h) && aligned { img.data } else { resample(&img, w, h, xf) };
|
||||
let cmyk = match dcs.equivalents.iter().find(|(n, _)| *n == name) {
|
||||
Some((_, c)) => *c,
|
||||
None if PROCESS_PLATES.contains(&name.as_str()) => [0.0; 4],
|
||||
None => {
|
||||
warnings.push(format!("the file gives no process equivalent for the {name} ink: it shows as black"));
|
||||
[0.0, 0.0, 0.0, 1.0]
|
||||
}
|
||||
};
|
||||
plates.push(InkPlate { name, cmyk, data });
|
||||
}
|
||||
let separated = Separated::new(w, h, plates).ok_or("its plates can't be kept together")?;
|
||||
let clip = clip_path(main);
|
||||
// Points one pixel spans along each axis.
|
||||
let [a, b, c, d, ..] = xf.as_coeffs();
|
||||
let ppi = [a.hypot(b), c.hypot(d)].map(|l| (l > 1e-12).then(|| 72.0 / l));
|
||||
let ppi = match ppi {
|
||||
[Some(x), Some(y)] => Some((x, y)),
|
||||
_ => None,
|
||||
};
|
||||
Ok(Placed { separated, page, xf, clip, ppi, warnings })
|
||||
}
|
||||
|
||||
/// The pixel grid the plates `images` go on: the first image's pixels, grown by whole pixels to
|
||||
/// hold where every image prints, as its width, height and where its pixels print; `None` without
|
||||
/// an image. It is the first image's own when its transform can't be inverted.
|
||||
fn grid<'a>(mut images: impl Iterator<Item = &'a PlateImage>) -> Option<(u32, u32, Affine)> {
|
||||
let first = images.next()?;
|
||||
let own = Some((first.width, first.height, first.xf));
|
||||
if first.xf.determinant().abs() <= 1e-12 {
|
||||
return own;
|
||||
}
|
||||
let to_first = first.xf.inverse();
|
||||
let mut r = Rect::new(0.0, 0.0, f64::from(first.width), f64::from(first.height));
|
||||
for img in images {
|
||||
r = r.union((to_first * img.xf).transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(img.width), f64::from(img.height))));
|
||||
}
|
||||
// Edges within a millionth of a pixel of a whole one are on it.
|
||||
let snap = |v: f64, up: bool| {
|
||||
let n = v.round();
|
||||
if (v - n).abs() < 1e-6 {
|
||||
n
|
||||
} else if up {
|
||||
v.ceil()
|
||||
} else {
|
||||
v.floor()
|
||||
}
|
||||
};
|
||||
let (x0, y0, x1, y1) = (snap(r.x0, false), snap(r.y0, false), snap(r.x1, true), snap(r.y1, true));
|
||||
let size = |a: f64, b: f64| Some(b - a).filter(|n| n.is_finite() && (1.0..=f64::from(u32::MAX)).contains(n)).map(|n| n as u32);
|
||||
match (size(x0, x1), size(y0, y1)) {
|
||||
(Some(w), Some(h)) => Some((w, h, first.xf * Affine::translate((x0, y0)))),
|
||||
// Larger than any separated image may be ([`Separated::fits`]).
|
||||
_ => Some((u32::MAX, u32::MAX, first.xf)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The ink amounts of `img` on the `w` × `h` pixels that `onto` puts on the page (each the
|
||||
/// nearest pixel of `img` where it prints there, none outside it). When either transform can't be
|
||||
/// inverted, the pixels are stretched over one another instead.
|
||||
fn resample(img: &PlateImage, w: u32, h: u32, onto: Affine) -> Vec<u8> {
|
||||
let (sw, sh) = (img.width as usize, img.height as usize);
|
||||
let mut out = Vec::with_capacity(w as usize * h as usize);
|
||||
let invertible = |m: Affine| m.determinant().abs() > 1e-12;
|
||||
// From a pixel of the image made to a pixel of `img`.
|
||||
let to_plate = (invertible(img.xf) && invertible(onto)).then(|| img.xf.inverse() * onto);
|
||||
for y in 0..h as usize {
|
||||
for x in 0..w as usize {
|
||||
let at = match to_plate {
|
||||
Some(m) => {
|
||||
let p = m * vectorcraft_geom::Point::new(x as f64 + 0.5, y as f64 + 0.5);
|
||||
let inside = p.x >= 0.0 && p.y >= 0.0 && p.x < sw as f64 && p.y < sh as f64;
|
||||
inside.then(|| (p.y as usize).min(sh - 1) * sw + (p.x as usize).min(sw - 1))
|
||||
}
|
||||
None => Some((y * sh / h as usize).min(sh - 1) * sw + (x * sw / w as usize).min(sw - 1)),
|
||||
};
|
||||
out.push(at.and_then(|i| img.data.get(i).copied()).unwrap_or(0));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Is an image under `n` (itself included) drawn through a clipping path: inside a clipping group
|
||||
/// or layer, or `inside` one already?
|
||||
fn clips_an_image(n: &Node, inside: bool) -> bool {
|
||||
let inside = inside || matches!(n.kind, NodeKind::Group { clip: true, .. } | NodeKind::Layer { clip: true, .. });
|
||||
match &n.kind {
|
||||
NodeKind::Image(_) => inside,
|
||||
_ => n.children().is_some_and(|ch| ch.iter().any(|c| clips_an_image(c, inside))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The image plate program `eps` draws, as ink amounts (how dark each pixel is: 255 less its
|
||||
/// luma), and where it prints on its page; an error when it draws more than one image, or art
|
||||
/// besides it.
|
||||
fn plate_image(eps: &[u8]) -> Result<PlateImage, String> {
|
||||
let r = crate::import_with(eps, false)?;
|
||||
if r.preview {
|
||||
return Err(r.warnings.first().cloned().unwrap_or_else(|| "its PostScript can't be read".into()));
|
||||
}
|
||||
// A plate is one image: one that draws more (several images, or art) would lose ink.
|
||||
let (mut found, mut images, mut art) = (None, 0usize, false);
|
||||
r.document.walk(|n| match &n.kind {
|
||||
NodeKind::Image(im) => {
|
||||
images += 1;
|
||||
found.get_or_insert((im.key.clone(), im.xf, im.width, im.height));
|
||||
}
|
||||
NodeKind::Path { clipping: true, .. } | NodeKind::Layer { .. } | NodeKind::Group { .. } | NodeKind::RulerGuide { .. } => {}
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } => {
|
||||
art |= n.visible && n.appearance.items.iter().any(|i| i.visible() && !matches!(i.paint(), vectorcraft_color::Paint::None))
|
||||
}
|
||||
_ => art |= n.visible,
|
||||
});
|
||||
if images > 1 || (art && images == 1) {
|
||||
return Err(if images > 1 { "it draws more than one image".into() } else { "it draws art besides its image".into() });
|
||||
}
|
||||
// Ink outside a clipping path would print: a plate whose image is clipped is refused.
|
||||
if images == 1 && r.document.layers.iter().any(|l| clips_an_image(l, false)) {
|
||||
return Err("it draws its image through a clipping path".into());
|
||||
}
|
||||
let (key, xf, iw, ih) = found.ok_or("it draws no image")?;
|
||||
let blob = r.document.images.get(&key).ok_or("it draws no image")?;
|
||||
let (width, height, data) = blob.coverage().ok_or("its image is too large to read")?;
|
||||
// The object's pixel space may differ from the pixels it keeps.
|
||||
let xf = xf * Affine::scale_non_uniform(f64::from(iw) / f64::from(width.max(1)), f64::from(ih) / f64::from(height.max(1)));
|
||||
Ok(PlateImage { width, height, data, xf })
|
||||
}
|
||||
|
||||
/// The clipping path the first image of `main` is drawn through, when it is drawn through one.
|
||||
fn clip_path(main: &[u8]) -> Option<Node> {
|
||||
let r = crate::import_with(main, false).ok().filter(|r| !r.preview)?;
|
||||
first_clip(&r.document)
|
||||
}
|
||||
|
||||
/// The clipping path the first image of `d` is drawn through: the clipping path of the innermost
|
||||
/// clipping group that holds an image, cut to the clipping paths of the clipping groups around it.
|
||||
fn first_clip(d: &Document) -> Option<Node> {
|
||||
/// The clipping paths around the innermost clipping group under `n` that holds an image,
|
||||
/// outermost first.
|
||||
fn find(n: &Node) -> Option<Vec<Node>> {
|
||||
let NodeKind::Group { children, clip } = &n.kind else {
|
||||
return n.children().and_then(|ch| ch.iter().find_map(|c| find(c)));
|
||||
};
|
||||
let first = children.first().filter(|f| *clip && matches!(f.kind, NodeKind::Path { clipping: true, .. }));
|
||||
if let Some(mut inner) = children.iter().find_map(|c| find(c)) {
|
||||
if let Some(f) = first {
|
||||
inner.insert(0, (**f).clone());
|
||||
}
|
||||
return Some(inner);
|
||||
}
|
||||
let holds_image = || {
|
||||
let mut any = false;
|
||||
n.walk(&mut |c: &Node| any |= matches!(c.kind, NodeKind::Image(_)));
|
||||
any
|
||||
};
|
||||
first.filter(|_| holds_image()).map(|f| vec![(**f).clone()])
|
||||
}
|
||||
let mut clips = d.layers.iter().find_map(|l| find(l))?;
|
||||
let mut innermost = clips.pop()?;
|
||||
// The clipping paths together clip to where they overlap.
|
||||
for outer in clips.iter().rev() {
|
||||
if let (NodeKind::Path { path: o, rule: or, .. }, NodeKind::Path { path, rule, .. }) = (&outer.kind, &mut innermost.kind) {
|
||||
*path = vectorcraft_pathops::boolean(path, *rule, o, *or, vectorcraft_pathops::BoolOp::Intersect);
|
||||
*rule = vectorcraft_geom::FillRule::NonZero;
|
||||
}
|
||||
}
|
||||
// An even-odd clipping path is kept as the same area under the nonzero rule, the rule of the
|
||||
// clip groups the Links commands make and match again.
|
||||
if let NodeKind::Path { path, rule: rule @ vectorcraft_geom::FillRule::EvenOdd, .. } = &mut innermost.kind
|
||||
&& let Ok(nonzero) = vectorcraft_pathops::try_boolean_n(&[(&*path, vectorcraft_geom::FillRule::EvenOdd)], |inside| !inside.is_empty())
|
||||
{
|
||||
(*path, *rule) = (nonzero, vectorcraft_geom::FillRule::NonZero);
|
||||
}
|
||||
Some(innermost)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,283 @@
|
||||
//! DCS files made from scratch as http://www.telegraphics.net/doc/DCS.txt describes them: a main
|
||||
//! EPS file whose header names its plates, each plate an EPS program drawing one grayscale image.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use vectorcraft_doc::NodeKind;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A plate program: one `w` × `h` grayscale image (`gray`, row by row from the top) on a 20 × 10
|
||||
/// pt page.
|
||||
pub(crate) fn plate_eps(w: u32, h: u32, gray: &[u8]) -> String {
|
||||
let hex: String = gray.iter().map(|g| format!("{g:02x}")).collect();
|
||||
format!(
|
||||
"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EndComments\n20 10 scale\n{w} {h} 8 [{w} 0 0 -{h} 0 {h}] <{hex}> image\nshowpage\n%%Trailer\n%%EOF\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// The composite: the image `w` × `h` drawn through a clipping path, the left half of the page.
|
||||
fn composite(w: u32, h: u32) -> String {
|
||||
let hex = "80".repeat((w * h * 3) as usize);
|
||||
format!(
|
||||
"gsave newpath 0 0 moveto 10 0 lineto 10 10 lineto 0 10 lineto closepath clip newpath\n20 10 scale\n{w} {h} 8 [{w} 0 0 -{h} 0 {h}] <{hex}> false 3 colorimage\ngrestore\nshowpage\n%%EOF\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// A single-file DCS 2.0 file of `plates` (name, plate program), with the header lines `extra`.
|
||||
pub(crate) fn single_file(plates: &[(&str, String)], extra: &str) -> Vec<u8> {
|
||||
// Offsets and sizes are written ten digits wide, so the header's length is known first.
|
||||
let header = |offsets: &[usize]| {
|
||||
let mut s = String::from("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n");
|
||||
s.push_str(extra);
|
||||
for ((name, eps), at) in plates.iter().zip(offsets) {
|
||||
s.push_str(&format!("%%PlateFile: ({name}) EPS #{at:010} {:010}\n", eps.len()));
|
||||
}
|
||||
s.push_str("%%EndComments\n");
|
||||
s.push_str(&composite(2, 2));
|
||||
s
|
||||
};
|
||||
let main = header(&vec![0; plates.len()]).len();
|
||||
let mut offsets = vec![];
|
||||
let mut at = main;
|
||||
for (_, eps) in plates {
|
||||
offsets.push(at);
|
||||
at += eps.len();
|
||||
}
|
||||
let mut out = header(&offsets).into_bytes();
|
||||
assert_eq!(out.len(), main);
|
||||
for (_, eps) in plates {
|
||||
out.extend(eps.as_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_file_plates_and_equivalents_are_read() {
|
||||
let plate = plate_eps(1, 1, &[0]);
|
||||
let extra = "%%DocumentCustomColors: (PANTONE 185 C)\n%%+ (Gr\\374n \\(A\\))\n%%CMYKCustomColor: 0 .91 .76 0 (PANTONE 185 C)\n%%+ .5 0 1 0 (Gr\\374n \\(A\\))\n%%RGBCustomColor: 1 1 0 (Lemon)\n";
|
||||
let bytes = single_file(&[("cyan", plate.clone()), ("PANTONE 185 C", plate.clone()), ("Grün (A)", plate.clone())], extra);
|
||||
let dcs = read(&bytes).unwrap();
|
||||
assert_eq!(dcs.version, 2);
|
||||
let names: Vec<&str> = dcs.plates.iter().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["Cyan", "PANTONE 185 C", "Grün (A)"], "a process ink in its usual case; escapes read");
|
||||
let PlateAt::Here { offset, len } = dcs.plates[1].at else { panic!("in the file: {:?}", dcs.plates[1]) };
|
||||
assert_eq!(&bytes[offset..offset + len], plate.as_bytes());
|
||||
assert_eq!(
|
||||
dcs.equivalents,
|
||||
[
|
||||
("PANTONE 185 C".to_string(), [0.0, 0.91, 0.76, 0.0]),
|
||||
("Grün (A)".to_string(), [0.5, 0.0, 1.0, 0.0]),
|
||||
("Lemon".to_string(), [0.0, 0.0, 1.0, 0.0])
|
||||
]
|
||||
);
|
||||
assert_eq!(dcs.plate_files().count(), 0);
|
||||
let main = main_program(&bytes, &dcs).unwrap();
|
||||
assert!(main.ends_with(b"%%EOF\n"), "up to the first plate");
|
||||
let PlateAt::Here { offset: first, .. } = dcs.plates[0].at else { panic!("in the file") };
|
||||
assert_eq!(main.len(), first);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plate_files_of_both_versions_are_read() {
|
||||
let two = "%!PS-Adobe-3.0 EPSF-3.0\n%%PlateFile: (Black) EPS Local pic.K\n%%PlateFile: (Black) TIFF NewServe c:\\scans\\a0K.scn\n%%PlateFile: (TRUMATCH 1-a5) EPS Local my pic.9\n%%EndComments\n";
|
||||
let dcs = read(two.as_bytes()).unwrap();
|
||||
assert_eq!(dcs.version, 2);
|
||||
assert_eq!(
|
||||
dcs.plates,
|
||||
[
|
||||
PlateRef { name: "Black".into(), at: PlateAt::File("pic.K".into()) },
|
||||
PlateRef { name: "TRUMATCH 1-a5".into(), at: PlateAt::File("my pic.9".into()) },
|
||||
]
|
||||
);
|
||||
let one = "%!PS-Adobe-2.0 EPSF-1.2\n%%CyanPlate: pic.C\n%%MagentaPlate: pic.M\n%%YellowPlate: pic.Y\n%%BlackPlate: pic.K\n%%EndComments\n";
|
||||
let dcs = read(one.as_bytes()).unwrap();
|
||||
assert_eq!(dcs.version, 1);
|
||||
assert_eq!(dcs.plate_files().collect::<Vec<_>>(), ["pic.C", "pic.M", "pic.Y", "pic.K"]);
|
||||
let both = "%!PS-Adobe-2.0 EPSF-1.2\n%%CyanPlate: old.C\n%%PlateFile: (Cyan) EPS Local new.C\n%%EndComments\n";
|
||||
assert_eq!(read(both.as_bytes()).unwrap().plate_files().collect::<Vec<_>>(), ["new.C"], "the DCS 2.0 comments win");
|
||||
// Comments after the header are not the file's.
|
||||
assert!(read(b"%!PS-Adobe-3.0 EPSF-3.0\n%%EndComments\n%%PlateFile: (Cyan) EPS Local a.C\n").is_none());
|
||||
assert!(read(plate_eps(1, 1, &[0]).as_bytes()).is_none(), "a plain EPS file");
|
||||
assert!(read(b"\x89PNG").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plate_program_drawing_more_than_its_image_is_an_error() {
|
||||
let plate = plate_eps(1, 1, &[0]);
|
||||
// Two image tiles, and an image with a filled square beside it.
|
||||
let tiles = plate.replacen("showpage", "1 1 8 [1 0 0 -1 0 1] <00> image\nshowpage", 1);
|
||||
let art = plate.replacen("20 10 scale", "0 0 moveto 5 0 lineto 5 5 lineto closepath fill\n20 10 scale", 1);
|
||||
for (eps, why) in [(tiles, "more than one image"), (art, "art besides its image")] {
|
||||
let bytes = single_file(&[("Cyan", eps)], "");
|
||||
let err = place(&bytes, &read(&bytes).unwrap(), &mut |_| Err("no files".into())).unwrap_err();
|
||||
assert!(err.contains("the Cyan plate can't be read") && err.contains(why), "{err}");
|
||||
}
|
||||
let bytes = single_file(&[("Cyan", plate)], "");
|
||||
assert!(place(&bytes, &read(&bytes).unwrap(), &mut |_| Err("no files".into())).is_ok(), "one image alone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plate_program_clipping_its_image_is_an_error() {
|
||||
let clipped = plate_eps(1, 1, &[0]).replacen("20 10 scale", "newpath 0 0 moveto 5 0 lineto 5 5 lineto closepath clip newpath\n20 10 scale", 1);
|
||||
let bytes = single_file(&[("Cyan", clipped)], "");
|
||||
let err = place(&bytes, &read(&bytes).unwrap(), &mut |_| Err("no files".into())).unwrap_err();
|
||||
assert!(err.contains("the Cyan plate can't be read") && err.contains("clipping path"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_an_image_inside_a_clipping_group_or_layer_is_clipped() {
|
||||
use std::sync::Arc;
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::NodeId;
|
||||
let clip_path = || {
|
||||
let rect = vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0));
|
||||
let mut p = Node::path(NodeId(1), rect, vectorcraft_doc::Appearance::basic(Paint::None, Paint::None, 0.0));
|
||||
if let NodeKind::Path { clipping, .. } = &mut p.kind {
|
||||
*clipping = true;
|
||||
}
|
||||
Arc::new(p)
|
||||
};
|
||||
let image = || {
|
||||
let im = vectorcraft_doc::ImageObject {
|
||||
key: "k".into(),
|
||||
width: 1,
|
||||
height: 1,
|
||||
xf: Affine::IDENTITY,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
Arc::new(Node::new(NodeId(2), NodeKind::Image(Box::new(im))))
|
||||
};
|
||||
let group = |children: Vec<Arc<Node>>, clip| Node::new(NodeId(3), NodeKind::Group { children, clip });
|
||||
let beside = group(vec![Arc::new(group(vec![clip_path()], true)), image()], false);
|
||||
assert!(!clips_an_image(&beside, false), "a clipping group beside the image");
|
||||
assert!(clips_an_image(&group(vec![clip_path(), image()], true), false), "the image inside the clipping group");
|
||||
assert!(clips_an_image(&group(vec![image()], false), true), "inside a clipping layer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_clips_around_the_composite_clip_it_together() {
|
||||
// The left two thirds of the page, then the right three quarters inside it: 5 to 13 pt.
|
||||
let main = "%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EndComments\n\
|
||||
gsave newpath 0 0 moveto 13 0 lineto 13 10 lineto 0 10 lineto closepath clip newpath\n\
|
||||
gsave newpath 5 0 moveto 20 0 lineto 20 10 lineto 5 10 lineto closepath clip newpath\n\
|
||||
20 10 scale 2 2 8 [2 0 0 -2 0 2] <808080808080808080808080> false 3 colorimage grestore grestore showpage\n%%EOF\n";
|
||||
let clip = clip_path(main.as_bytes()).expect("a clipping path");
|
||||
let b = clip.geometric_bounds().expect("bounds");
|
||||
assert!((b.x0 - 5.0).abs() < 1e-6 && (b.x1 - 13.0).abs() < 1e-6, "where both clips overlap: {b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plate_file_names_keep_their_spaces() {
|
||||
let two = "%!PS-Adobe-3.0 EPSF-3.0\n%%PlateFile: (Cyan) EPS Local my pic .C\n%%EndComments\n";
|
||||
assert_eq!(read(two.as_bytes()).unwrap().plate_files().collect::<Vec<_>>(), ["my pic .C"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_bytes_that_are_not_utf8_are_latin1() {
|
||||
// `Grün` with a raw Latin-1 ü (0xFC), in a plate name and in a custom color's name.
|
||||
let mut two = b"%!PS-Adobe-3.0 EPSF-3.0\n%%CMYKCustomColor: .5 0 1 0 (Gr\xfcn)\n%%PlateFile: (Gr\xfcn) EPS Local a.9\n".to_vec();
|
||||
two.extend(b"%%PlateFile: (Grau \xc3\xa9) EPS Local b.9\n%%EndComments\n");
|
||||
let dcs = read(&two).unwrap();
|
||||
let names: Vec<&str> = dcs.plates.iter().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["Grün", "Grau é"], "a line that is UTF-8 is read as UTF-8");
|
||||
assert_eq!(dcs.equivalents, [("Grün".to_string(), [0.5, 0.0, 1.0, 0.0])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_plate_is_read_with_the_clipping_path() {
|
||||
// 2 × 2 pixels: cyan solid on the top row, the spot ink at 50% on the first column.
|
||||
let bytes = single_file(
|
||||
&[("Cyan", plate_eps(2, 2, &[0, 0, 255, 255])), ("PANTONE 185 C", plate_eps(2, 2, &[127, 255, 127, 255]))],
|
||||
"%%CMYKCustomColor: 0 .91 .76 0 (PANTONE 185 C)\n",
|
||||
);
|
||||
let dcs = read(&bytes).unwrap();
|
||||
let p = place(&bytes, &dcs, &mut |_| Err("no files".into())).unwrap();
|
||||
assert!(p.warnings.is_empty(), "{:?}", p.warnings);
|
||||
assert_eq!(p.page, Rect::new(0.0, 0.0, 20.0, 10.0));
|
||||
let s = &p.separated;
|
||||
assert_eq!((s.width, s.height), (2, 2));
|
||||
assert_eq!(s.plates[0].data, [255, 255, 0, 0], "a dark plate pixel is ink");
|
||||
assert_eq!(s.plates[1].data, [128, 0, 128, 0]);
|
||||
assert_eq!(s.plates[1].cmyk, [0.0, 0.91, 0.76, 0.0]);
|
||||
assert_eq!(p.xf * vectorcraft_geom::Point::new(2.0, 2.0), vectorcraft_geom::Point::new(20.0, 10.0));
|
||||
assert_eq!(p.ppi, Some((7.2, 14.4)));
|
||||
let clip = p.clip.expect("the composite's clipping path");
|
||||
assert!(matches!(clip.kind, NodeKind::Path { clipping: true, .. }));
|
||||
assert_eq!(clip.geometric_bounds().map(|b| b.width().round()), Some(10.0), "the left half of the page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plate_files_are_read_with_the_callback() {
|
||||
let main = "%!PS-Adobe-2.0 EPSF-1.2\n%%BoundingBox: 0 0 20 10\n%%CyanPlate: pic.C\n%%BlackPlate: pic.K\n%%EndComments\nshowpage\n";
|
||||
let files: HashMap<&str, String> = [("pic.C", plate_eps(1, 1, &[0])), ("pic.K", plate_eps(2, 1, &[255, 0]))].into();
|
||||
let dcs = read(main.as_bytes()).unwrap();
|
||||
let mut asked = vec![];
|
||||
let p = place(main.as_bytes(), &dcs, &mut |f| {
|
||||
asked.push(f.to_string());
|
||||
files.get(f).map(|e| e.as_bytes().to_vec()).ok_or_else(|| "missing".into())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(asked, ["pic.C", "pic.K"]);
|
||||
assert!(p.clip.is_none(), "no composite, no clipping path");
|
||||
assert_eq!((p.separated.width, p.separated.height), (1, 1), "the first plate's size");
|
||||
assert_eq!(p.separated.plates[1].data, [255], "resampled to it: the pixel at its centre");
|
||||
let err = place(main.as_bytes(), &dcs, &mut |_| Err("not found".into())).unwrap_err();
|
||||
assert_eq!(err, "the Cyan plate can't be read: not found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plates_printing_elsewhere_on_the_page_stay_in_register() {
|
||||
// The Black plate's one solid pixel prints on the right half of the page only.
|
||||
let right = "%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EndComments\n10 0 translate 10 10 scale\n1 1 8 [1 0 0 -1 0 1] <00> image\nshowpage\n%%EOF\n";
|
||||
let bytes = single_file(&[("Cyan", plate_eps(2, 1, &[255, 255])), ("Black", right.to_string())], "");
|
||||
let p = place(&bytes, &read(&bytes).unwrap(), &mut |_| Err("no files".into())).unwrap();
|
||||
assert_eq!((p.separated.width, p.separated.height), (2, 1));
|
||||
assert_eq!(p.separated.plates[1].data, [0, 255], "the ink lands on the right pixel only");
|
||||
}
|
||||
|
||||
/// A plate program whose one solid pixel prints on a 10 × 10 pt square `x` pt from the page's left.
|
||||
fn square_at(x: u32) -> String {
|
||||
format!(
|
||||
"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EndComments\n{x} 0 translate 10 10 scale\n1 1 8 [1 0 0 -1 0 1] <00> image\nshowpage\n%%EOF\n"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_plates_grid_grows_to_hold_every_plate() {
|
||||
let bytes = single_file(&[("Cyan", square_at(0)), ("Black", square_at(10))], "");
|
||||
let p = place(&bytes, &read(&bytes).unwrap(), &mut |_| Err("no files".into())).unwrap();
|
||||
assert_eq!((p.separated.width, p.separated.height), (2, 1), "the first plate's pixels, grown over the second");
|
||||
assert_eq!(p.separated.plates[0].data, [255, 0]);
|
||||
assert_eq!(p.separated.plates[1].data, [0, 255], "nothing of the second plate is cut off");
|
||||
assert_eq!(p.xf * vectorcraft_geom::Point::new(2.0, 1.0), vectorcraft_geom::Point::new(20.0, 10.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_naming_more_plates_than_are_read_is_an_error() {
|
||||
let plate = plate_eps(1, 1, &[0]);
|
||||
let names: Vec<String> = (0..=MAX_PLATES).map(|i| format!("Ink {i}")).collect();
|
||||
let plates: Vec<(&str, String)> = names.iter().map(|n| (n.as_str(), plate.clone())).collect();
|
||||
let bytes = single_file(&plates, "");
|
||||
let dcs = read(&bytes).unwrap();
|
||||
assert_eq!(dcs.plates.len(), MAX_PLATES + 1, "every plate named");
|
||||
let err = place(&bytes, &dcs, &mut |_| Err("no files".into())).unwrap_err();
|
||||
assert_eq!(err, "it names 65 plates, and at most 64 are read");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_plates_are_errors_and_missing_equivalents_warnings() {
|
||||
let mut bytes = single_file(&[("Cyan", plate_eps(1, 1, &[0]))], "");
|
||||
let dcs = read(&bytes).unwrap();
|
||||
bytes.truncate(bytes.len() - 5);
|
||||
assert_eq!(place(&bytes, &dcs, &mut |_| Err(String::new())).unwrap_err(), "the Cyan plate lies outside the file");
|
||||
let text = single_file(&[("Cyan", "%!PS-Adobe-3.0\n0 0 moveto 1 1 lineto stroke\n".to_string())], "");
|
||||
let err = place(&text, &read(&text).unwrap(), &mut |_| Err(String::new())).unwrap_err();
|
||||
assert_eq!(err, "the Cyan plate can't be read: it draws no image");
|
||||
let spot = single_file(&[("Mystery", plate_eps(1, 1, &[0]))], "");
|
||||
let p = place(&spot, &read(&spot).unwrap(), &mut |_| Err(String::new())).unwrap();
|
||||
assert_eq!(p.separated.plates[0].cmyk, [0.0, 0.0, 0.0, 1.0]);
|
||||
assert_eq!(p.warnings, ["the file gives no process equivalent for the Mystery ink: it shows as black"]);
|
||||
}
|
||||
@@ -89,10 +89,11 @@ pub(crate) use ai::{
|
||||
BRUSH_MANAGER, BrushesRead, MAX_EDITING_OBJECTS, MAX_GRADIENT_STOPS, MAX_LISTED, MAX_LISTED_NAME, MAX_SYMBOLS, brush_plugin_name, brushes_kept,
|
||||
read_art_style, read_brush_object, read_style_list, style_moves_with, symbol_art_member, warp_number,
|
||||
};
|
||||
pub(crate) use ate::{MAX_BYTES as MAX_TEXT_DOCUMENT, MAX_FRAMES, MAX_SEGMENTS, MAX_STORY, MAX_VALUES as MAX_TEXT_VALUES};
|
||||
pub(crate) use ate::{MAX_BYTES as MAX_TEXT_DOCUMENT, MAX_FRAMES, MAX_SEGMENTS, MAX_STORY, MAX_VALUES as MAX_TEXT_VALUES, record_version};
|
||||
pub use native::{AiLayers, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard, is_loss, layered_ai, layered_ai_from};
|
||||
pub(crate) use native::{MAX_TEXT, scale_document};
|
||||
pub use text::family_style;
|
||||
pub(crate) use text::{document_font, postscript_face};
|
||||
|
||||
/// The page of a PostScript file without a bounding box (US Letter).
|
||||
const LETTER: [f64; 4] = [0.0, 0.0, 612.0, 792.0];
|
||||
|
||||
@@ -508,10 +508,16 @@ impl Reader<'_> {
|
||||
None => (full.as_str(), None),
|
||||
};
|
||||
st.cmap = cmap;
|
||||
let (family, style) = super::super::super::text::family_style(name);
|
||||
// The installed face of that PostScript name, as type from a text document
|
||||
// takes it; else the name split into a family and style.
|
||||
let text = super::super::super::text::named_font(name, None);
|
||||
let (family, style, version) = text.unwrap_or_else(|| {
|
||||
let (family, style) = super::super::super::text::family_style(name);
|
||||
(family, style, None)
|
||||
});
|
||||
st.style.font_family = family;
|
||||
st.style.font_style = style;
|
||||
st.style.font_version = None;
|
||||
st.style.font_version = version;
|
||||
}
|
||||
if let Some(size) = vals.get(at + 1).and_then(V::num).filter(|s| s.is_finite() && *s > 0.0) {
|
||||
st.style.size = size.min(MAX_SIZE);
|
||||
|
||||
@@ -3930,6 +3930,28 @@ fn point_type(attrs: &str, text: &str) -> String {
|
||||
format!("0 To\n1 0 0 1 10 50 0 Tp\nTP\n0 Tr\n0 0 0 1 k\n/_Helvetica 12 Tf\n{attrs}\n({text}) Tx\nTO\n")
|
||||
}
|
||||
|
||||
/// Legacy type is set in the installed face of exactly the PostScript name its `Tf` gives, as type
|
||||
/// from a text document is: not a face whose name differs only in case and loads first, nor the
|
||||
/// copy the family and style resolve to when the name's own is another (it names its version).
|
||||
#[test]
|
||||
fn legacy_type_takes_the_face_of_its_exact_postscript_name() {
|
||||
use vectorcraft_text::test_fonts::named_copy;
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
for (inter, names) in [
|
||||
(true, ("TWINLEGACY INK", "Regular", "Version 1.000", "TWINLEGACYINK-Regular")),
|
||||
(false, ("Twinlegacy Ink", "Regular", "Version 2.000", "TwinlegacyInk-Regular")),
|
||||
] {
|
||||
assert_eq!(db.add_font(named_copy(inter, names).unwrap()), 1, "{names:?}");
|
||||
}
|
||||
let face = |font: &str| {
|
||||
let texts = legacy_texts(&point_type("", "Hi").replace("/_Helvetica 12 Tf", &format!("/_{font} 12 Tf")));
|
||||
let f = db.style_face(&texts[0].runs[0].style).unwrap();
|
||||
(f.postscript_name(), f.version.clone())
|
||||
};
|
||||
assert_eq!(face("TwinlegacyInk-Regular"), ("TwinlegacyInk-Regular".to_string(), "Version 2.000".to_string()));
|
||||
assert_eq!(face("TWINLEGACYINK-Regular"), ("TWINLEGACYINK-Regular".to_string(), "Version 1.000".to_string()));
|
||||
}
|
||||
|
||||
/// `TC` and `TW` (§15.4.7) set the paragraph's letter and word spacing, `Tq` its hanging
|
||||
/// punctuation and `Th` (§22) its hyphenation; each paragraph keeps the values in force at its
|
||||
/// first character, and they last into the next text object.
|
||||
|
||||
@@ -459,7 +459,8 @@ fn spend_tabs(tabs: &mut Vec<TabStop>, budget: &mut usize) -> bool {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Style {
|
||||
font: Option<String>,
|
||||
/// The font its record names, as the fonts the app has set it ([`DocFont::font`]).
|
||||
font: Option<Font>,
|
||||
size: f64,
|
||||
/// `None`: auto.
|
||||
leading: Option<f64>,
|
||||
@@ -490,10 +491,44 @@ struct Style {
|
||||
standard_vertical_roman: bool,
|
||||
}
|
||||
|
||||
/// The longest font version string read from a font record: name ID 5 strings are short (`Version
|
||||
/// 7.200`, or with the tools that built the font, some 80 characters).
|
||||
const MAX_FONT_VERSION: usize = 256;
|
||||
|
||||
/// The version a font record's key 5 gives `v`, as the reader takes it: `None` when it is empty or
|
||||
/// longer than [`MAX_FONT_VERSION`] bytes. The `.ai` save checks a record it writes against this.
|
||||
pub(crate) fn record_version(v: &str) -> Option<&str> {
|
||||
(!v.is_empty() && v.len() <= MAX_FONT_VERSION).then_some(v)
|
||||
}
|
||||
|
||||
/// A font as type is set in it: family, style and the version it names
|
||||
/// ([`super::text::document_font`]).
|
||||
type Font = (String, String, Option<String>);
|
||||
|
||||
/// A font of a text document's font list, as its record (a `CoolTypeFont`) names it.
|
||||
#[derive(Debug, Default)]
|
||||
struct DocFont {
|
||||
/// Key 0: the PostScript name.
|
||||
name: String,
|
||||
/// Key 5, when the record keeps it: the font's version string (name ID 5), which tells installed
|
||||
/// copies of one font apart.
|
||||
version: Option<String>,
|
||||
/// [`Self::font`]'s answer, found once for every run that names the record.
|
||||
font: std::sync::OnceLock<Font>,
|
||||
}
|
||||
|
||||
impl DocFont {
|
||||
/// The font type set in this record's font takes: the installed copy it names
|
||||
/// ([`super::text::document_font`]).
|
||||
fn font(&self) -> &Font {
|
||||
self.font.get_or_init(|| super::text::document_font(&self.name, self.version.as_deref()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The text document of an editing copy.
|
||||
pub(super) struct Texts {
|
||||
stories: Vec<Val>,
|
||||
fonts: Vec<String>,
|
||||
fonts: Vec<DocFont>,
|
||||
/// The character and paragraph styles a story starts from.
|
||||
defaults: Val,
|
||||
para_defaults: Val,
|
||||
@@ -552,7 +587,15 @@ impl Texts {
|
||||
.get("0")?
|
||||
.list()
|
||||
.iter()
|
||||
.map(|f| f.get("0").and_then(|f| f.get("0")).and_then(|f| f.get("0")).and_then(Val::str).unwrap_or_default().to_string())
|
||||
.map(|f| {
|
||||
let record = f.get("0").and_then(|f| f.get("0"));
|
||||
let text = |k: &str| record.and_then(|r| r.get(k)).and_then(Val::str);
|
||||
DocFont {
|
||||
name: text("0").unwrap_or_default().to_string(),
|
||||
version: text("5").and_then(record_version).map(str::to_string),
|
||||
font: std::sync::OnceLock::new(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let main = doc.get("1")?;
|
||||
let sheets = |key: &str| -> Vec<Val> {
|
||||
@@ -774,12 +817,12 @@ impl Texts {
|
||||
let pick = |k: &str| chain.iter().find_map(|s| s.get(k)).or_else(|| self.defaults.get(k));
|
||||
let num = |k: &str| pick(k).and_then(Val::num);
|
||||
let on = |k: &str| !matches!(pick(k), Some(Val::Bool(false)));
|
||||
let font = num("0").and_then(|i| self.fonts.get(i as usize)).filter(|f| !f.is_empty()).cloned();
|
||||
let font = num("0").and_then(|i| self.fonts.get(i as usize)).filter(|f| !f.name.is_empty());
|
||||
let size = num("1").filter(|v| *v > 0.0 && *v < 1e5).unwrap_or(12.0);
|
||||
// Key 5 of a paint names the art style that paints it ([`Self::with_patterns`]).
|
||||
let pattern = |k: &str| pick(k).and_then(|p| p.get("5")).and_then(Val::str).and_then(|name| self.patterns.get(name)).cloned();
|
||||
Style {
|
||||
font,
|
||||
font: font.map(|f| f.font().clone()),
|
||||
size,
|
||||
leading: if on("4") { None } else { num("5").filter(|v| *v > 0.0 && *v < 1e5) },
|
||||
tracking: num("8").filter(|v| v.abs() < 1e5).unwrap_or(0.0),
|
||||
@@ -1386,13 +1429,12 @@ fn para_style(p: &Para, scale: f64) -> ParaStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// The character style of character attributes `s`: its font from the fonts the app has, its
|
||||
/// lengths (size, leading, baseline shift, stroke width) scaled by `scale`, and its patterns placed
|
||||
/// in the type's text space by `to_text` (the document → its text space), where they stay as the
|
||||
/// document has them.
|
||||
/// The character style of character attributes `s`: its font from the fonts the app has (the
|
||||
/// installed copy its font record names, [`DocFont::font`]), its lengths (size, leading, baseline
|
||||
/// shift, stroke width) scaled by `scale`, and its patterns placed in the type's text space by
|
||||
/// `to_text` (the document → its text space), where they stay as the document has them.
|
||||
fn char_style(s: &Style, scale: f64, to_text: Affine) -> CharStyle {
|
||||
let (family, style) = s.font.as_deref().map_or_else(|| (CharStyle::default().font_family, "Regular".to_string()), crate::family_style);
|
||||
let family = vectorcraft_text::FontDb::global().find_family(&family).unwrap_or(family);
|
||||
let (family, style, version) = s.font.clone().unwrap_or_else(|| (CharStyle::default().font_family, "Regular".to_string(), None));
|
||||
// A fill or stroke that is on takes its pattern in place of its colour.
|
||||
let paint = |color: Option<Color>, pattern: &Option<Paint>| match (color, pattern) {
|
||||
// A pattern that can't be placed (the type's text space out of range) keeps the colour.
|
||||
@@ -1407,6 +1449,7 @@ fn char_style(s: &Style, scale: f64, to_text: Affine) -> CharStyle {
|
||||
CharStyle {
|
||||
font_family: family,
|
||||
font_style: style,
|
||||
font_version: version,
|
||||
size: s.size * scale,
|
||||
// Auto leading stays auto: the layout gives each line the paragraph's share of the largest
|
||||
// type on it, as the file's own layout does ("The largest leading value in a line of type
|
||||
|
||||
@@ -63,3 +63,37 @@ fn abbreviated_styles_in_postscript_names_are_spelled_out() {
|
||||
fn an_installed_face_of_the_postscript_name_names_the_font() {
|
||||
assert_eq!(crate::family_style("ABCDEF+SourceSans3-Semibold"), ("Source Sans 3".to_string(), "Semibold".to_string()));
|
||||
}
|
||||
|
||||
/// The PostScript name and version of the face each `show` of a program that sets `fonts` in turn
|
||||
/// draws its type in.
|
||||
fn faces_shown(fonts: &[&str]) -> Vec<(String, String)> {
|
||||
let body: String = fonts.iter().map(|f| format!("/{f} findfont 12 scalefont setfont 10 20 moveto (Hi) show\n")).collect();
|
||||
let eps = format!("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n{body}showpage\n%%EOF\n");
|
||||
let r = import(eps.as_bytes()).unwrap();
|
||||
let mut faces = vec![];
|
||||
r.document.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
let face = FontDb::global().style_face(&t.runs[0].style).unwrap();
|
||||
faces.push((face.postscript_name(), face.version.clone()));
|
||||
}
|
||||
});
|
||||
faces
|
||||
}
|
||||
|
||||
/// Type a PostScript program sets is drawn in the installed face of exactly the font's PostScript
|
||||
/// name, as type from a text document is: not another family spelled alike but for case and
|
||||
/// punctuation, nor a face whose name differs only in case and loads first.
|
||||
#[test]
|
||||
fn shown_type_takes_the_face_of_its_exact_postscript_name() {
|
||||
use vectorcraft_text::test_fonts::named_copy;
|
||||
for (inter, names) in [
|
||||
(true, ("FUZZSHOW INK", "Regular", "Version 1.000", "FUZZSHOWINK-Regular")),
|
||||
(false, ("Fuzzshow/Ink", "Regular", "Version 1.000", "FuzzshowInk")),
|
||||
(true, ("CASESHOW INK", "Regular", "Version 1.000", "CASESHOWINK")),
|
||||
(false, ("Caseshow Ink", "Regular", "Version 2.000", "CaseshowInk")),
|
||||
] {
|
||||
assert_eq!(FontDb::global().add_font(named_copy(inter, names).unwrap()), 1, "{names:?}");
|
||||
}
|
||||
let names: Vec<String> = faces_shown(&["FuzzshowInk", "CaseshowInk", "CASESHOWINK"]).into_iter().map(|f| f.0).collect();
|
||||
assert_eq!(names, ["FuzzshowInk", "CaseshowInk", "CASESHOWINK"]);
|
||||
}
|
||||
|
||||
@@ -3128,6 +3128,73 @@ fn type_from_the_text_document_takes_its_character_styles_attributes() {
|
||||
assert_eq!(style(2), (solid(Color::cmyk(0.0, 1.0, 0.0, 0.0)), Some(18.0)), "the run's own colour");
|
||||
}
|
||||
|
||||
/// The PostScript name and version of the face that type from a text document whose font record is
|
||||
/// `record` (in place of `/0 (Helvetica)`) is set in.
|
||||
fn face_set_from(record: &str) -> (String, String) {
|
||||
let doc = text_document(&[("Hamburg\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]).replacen("/0 (Helvetica)", record, 1);
|
||||
let art = native_with_text(&(layer("Back", true, &square(10, 10)) + &layer("Spare", false, &text_object(0))), &doc);
|
||||
let r = import(&eps(&page_square(10, 10), &art)).unwrap();
|
||||
let texts = text_of(&r.document.layers[1]);
|
||||
assert_eq!(texts.len(), 1, "{record}: {:?}", r.warnings);
|
||||
let face = vectorcraft_text::FontDb::global().style_face(&texts[0].runs[0].style).unwrap();
|
||||
(face.postscript_name(), face.version.clone())
|
||||
}
|
||||
|
||||
/// Installs the copies of each font `copies` lists, the first of each loaded first.
|
||||
fn install(copies: &[(bool, vectorcraft_text::test_fonts::CopyNames<'_>)]) {
|
||||
for (inter, names) in copies {
|
||||
let font = vectorcraft_text::test_fonts::named_copy(*inter, *names).unwrap();
|
||||
assert_eq!(vectorcraft_text::FontDb::global().add_font(font), 1, "{names:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Type from the text document is set in the installed face of exactly the PostScript name its
|
||||
/// font record gives, whatever other installed copies answer to that name too: one whose family
|
||||
/// is spelled alike but for case and punctuation (`SAMPLE INK` beside `Sample/Ink`, PostScript name
|
||||
/// `SampleInk`), one whose PostScript name differs only in case or punctuation (in its own family or
|
||||
/// another), or another copy of the same family and style under another PostScript name (an older
|
||||
/// build of the font: the type names its version). Those copies set the type at other widths.
|
||||
#[test]
|
||||
fn type_from_the_text_document_takes_the_face_of_its_exact_postscript_name() {
|
||||
install(&[
|
||||
(true, ("FUZZTEST INK", "Regular", "Version 1.000", "FUZZTESTINK-Regular")),
|
||||
(false, ("Fuzztest/Ink", "Regular", "Version 1.000", "FuzztestInk")),
|
||||
(true, ("CASETEST INK", "Regular", "Version 1.000", "CASETESTINK")),
|
||||
(false, ("Casetest Ink", "Regular", "Version 2.000", "CasetestInk")),
|
||||
(true, ("Copytest Ink", "Regular", "Version 1.000", "CopytestInk-Light")),
|
||||
(false, ("Copytest Ink", "Regular", "Version 2.000", "CopytestInk-Regular")),
|
||||
(true, ("NeuetestInk", "Bold", "Version 1.000", "NeuetestInkBold")),
|
||||
(false, ("Neuetest Ink", "Bold", "Version 1.000", "NeuetestInk-Bold")),
|
||||
]);
|
||||
assert_eq!(face_set_from("/0 (FuzztestInk)").0, "FuzztestInk", "not the family spelled alike");
|
||||
assert_eq!(face_set_from("/0 (FUZZTESTINK-Regular)").0, "FUZZTESTINK-Regular");
|
||||
assert_eq!(face_set_from("/0 (CasetestInk)").0, "CasetestInk", "not the name in other case, loaded first");
|
||||
assert_eq!(face_set_from("/0 (CASETESTINK)").0, "CASETESTINK");
|
||||
assert_eq!(face_set_from("/0 (CopytestInk-Regular)"), ("CopytestInk-Regular".to_string(), "Version 2.000".to_string()));
|
||||
assert_eq!(face_set_from("/0 (CopytestInk-Light)"), ("CopytestInk-Light".to_string(), "Version 1.000".to_string()));
|
||||
assert_eq!(face_set_from("/0 (NeuetestInk-Bold)").0, "NeuetestInk-Bold", "not the name without its hyphen, in another family");
|
||||
// A name in a case no installed face has still finds the font.
|
||||
assert_eq!(face_set_from("/0 (casetestink)").0, "CASETESTINK");
|
||||
}
|
||||
|
||||
/// A font record may keep the font's version string (key 5, name ID 5): of two installed copies
|
||||
/// with one PostScript name, family and style, the type is set in that version, also when the
|
||||
/// record spells the name with other punctuation. A version that isn't installed, or a record
|
||||
/// without one, gives the copy the family and style resolve to.
|
||||
#[test]
|
||||
fn type_from_the_text_document_takes_the_version_its_font_record_keeps() {
|
||||
install(&[(true, ("Vertest Ink", "Regular", "1.0", "VertestInk-Regular")), (false, ("Vertest Ink", "Regular", "1.1", "VertestInk-Regular"))]);
|
||||
let version = |record: &str| face_set_from(record).1;
|
||||
assert_eq!(version("/0 (VertestInk-Regular) /5 (1.1)"), "1.1");
|
||||
assert_eq!(version("/0 (VertestInk-Regular) /5 (1.0)"), "1.0");
|
||||
assert_eq!(version("/0 (VertestInk-Regular)"), "1.0");
|
||||
assert_eq!(version("/0 (VertestInk-Regular) /5 (9.9)"), "1.0");
|
||||
assert_eq!(version("/0 (VertestInk-Regular) /5 ()"), "1.0");
|
||||
// A record whose name differs from the copies' in punctuation alone still picks its version.
|
||||
assert_eq!(version("/0 (Vertest-Ink-Regular) /5 (1.1)"), "1.1");
|
||||
assert_eq!(version("/0 (Vertest-Ink-Regular)"), "1.0");
|
||||
}
|
||||
|
||||
/// A story with no runs, and a run or paragraph that names no style, is set in `[Normal Character
|
||||
/// Style]` and `[Normal Paragraph Style]` before the text document's base attributes. A Large
|
||||
/// Canvas copy (1:10) writes the styles at the art's scale (1.2 for 12 points, 0.6 for a 6-point
|
||||
|
||||
@@ -42,20 +42,75 @@ fn style_word(w: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// A font's PostScript name without a subset prefix (`ABCDEF+`) or the `*1` of a re-encoded copy
|
||||
/// (`ABCDEF+RollerBabyBV*1` is `RollerBabyBV`).
|
||||
fn bare_name(name: &str) -> &str {
|
||||
let name = match name.split_once('+') {
|
||||
Some((prefix, rest)) if prefix.len() == 6 && prefix.bytes().all(|b| b.is_ascii_uppercase()) => rest,
|
||||
_ => name,
|
||||
};
|
||||
match name.rsplit_once('*') {
|
||||
Some((base, n)) if !base.is_empty() && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()) => base,
|
||||
_ => name,
|
||||
}
|
||||
}
|
||||
|
||||
/// The font of type that a text document's font record names: by `name`, its PostScript name, and
|
||||
/// `version`, the version string (name ID 5) the record keeps, if any. An installed face of that
|
||||
/// name gives its font ([`named_font`]). Without one, `name` is split ([`family_style`]) and the
|
||||
/// family spelled as an available family is ([`FontDb::find_family`]: `MicrosoftYaHei` is
|
||||
/// Microsoft YaHei). → (family, style, version).
|
||||
///
|
||||
/// [`FontDb::find_family`]: vectorcraft_text::FontDb::find_family
|
||||
pub(crate) fn document_font(name: &str, version: Option<&str>) -> (String, String, Option<String>) {
|
||||
named_font(name, version).unwrap_or_else(|| {
|
||||
let (family, style) = family_style(name);
|
||||
let family = vectorcraft_text::FontDb::global().find_family(&family).unwrap_or(family);
|
||||
(family, style, None)
|
||||
})
|
||||
}
|
||||
|
||||
/// The font of type set in the installed face of PostScript name `name` (a subset prefix and a
|
||||
/// re-encoded copy's `*1` aside) in `version` ([`postscript_face`]): that face's own family and
|
||||
/// style, and its version when the family and style alone resolve to another installed copy of
|
||||
/// the font. A library can keep two copies under one family and style that differ in their
|
||||
/// PostScript names or their versions, and draw the same text at different widths. → (family,
|
||||
/// style, version); `None` when no installed face has the name.
|
||||
pub(crate) fn named_font(name: &str, version: Option<&str>) -> Option<(String, String, Option<String>)> {
|
||||
let face = postscript_face(bare_name(name), version)?;
|
||||
// As the PDF import names a version: only one that the family and style don't resolve to.
|
||||
let default = vectorcraft_text::FontDb::global().face(&face.family, &face.style);
|
||||
let version = default.filter(|d| d.version != face.version).map(|_| face.version.clone());
|
||||
Some((face.family.clone(), face.style.clone(), version))
|
||||
}
|
||||
|
||||
/// The installed face of PostScript name `name` (without a subset prefix), among the installed
|
||||
/// faces that answer to it ([`FontDb::postscript_faces`]): those of exactly that name when there
|
||||
/// are any (PostScript names are case-sensitive, so `SampleSans` isn't the face named
|
||||
/// `SAMPLESANS`, and `SampleSerif-Bold` isn't `SampleSerifBold`), and of those the one in
|
||||
/// `version` when it is installed, else the first. `None` when no installed face has the name.
|
||||
///
|
||||
/// [`FontDb::postscript_faces`]: vectorcraft_text::FontDb::postscript_faces
|
||||
pub(crate) fn postscript_face(name: &str, version: Option<&str>) -> Option<Arc<vectorcraft_text::FontFace>> {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let found = db.find_postscript(name)?;
|
||||
if found.postscript_name() == name && version.is_none_or(|v| v == found.version) {
|
||||
return Some(found);
|
||||
}
|
||||
let faces = db.postscript_faces(name);
|
||||
let named: Vec<_> = faces.iter().filter(|f| f.postscript_name() == name).cloned().collect();
|
||||
let pool = if named.is_empty() { faces } else { named };
|
||||
let in_version = version.and_then(|v| pool.iter().find(|f| f.version == v).cloned());
|
||||
Some(in_version.or_else(|| pool.into_iter().next()).unwrap_or(found))
|
||||
}
|
||||
|
||||
/// A font's family and style from its PostScript name, without a subset prefix (`ABCDEF+`) or the
|
||||
/// `*1` of a re-encoded copy (`ABCDEF+RollerBabyBV*1`): an installed or bundled face of that
|
||||
/// exact name gives its own (a family can hold hyphens: `rounded-x-mplus-1c-black` is Rounded-X
|
||||
/// M+ 1c, black); else the name is split, `Helvetica-BoldOblique` → (`Helvetica`, `Bold
|
||||
/// Oblique`), `TimesNewRomanPS-BoldMT` → (`Times New Roman`, `Bold`).
|
||||
pub fn family_style(name: &str) -> (String, String) {
|
||||
let name = match name.split_once('+') {
|
||||
Some((prefix, rest)) if prefix.len() == 6 && prefix.bytes().all(|b| b.is_ascii_uppercase()) => rest,
|
||||
_ => name,
|
||||
};
|
||||
let name = match name.rsplit_once('*') {
|
||||
Some((base, n)) if !base.is_empty() && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()) => base,
|
||||
_ => name,
|
||||
};
|
||||
let name = bare_name(name);
|
||||
if let Some(face) = vectorcraft_text::FontDb::global().find_postscript(name) {
|
||||
return (face.family.clone(), face.style.clone());
|
||||
}
|
||||
@@ -551,11 +606,11 @@ impl Interp<'_> {
|
||||
if !(size.is_finite() && size > 1e-3 && size < 1e5) {
|
||||
return ps_err("undefinedresult", "a font size");
|
||||
}
|
||||
let (family, font_style) = family_style(&name);
|
||||
// The available family's own spelling: `MicrosoftYaHei` is Microsoft YaHei, not "Microsoft Ya Hei".
|
||||
let font_family = vectorcraft_text::FontDb::global().find_family(&family).unwrap_or(family);
|
||||
// The installed face of that PostScript name, as type from a text document takes it; else
|
||||
// the available family's own spelling: `MicrosoftYaHei` is Microsoft YaHei, not "Microsoft Ya Hei".
|
||||
let (font_family, font_style, font_version) = document_font(&name, None);
|
||||
let fill = if self.g.paint.is_none() { Paint::solid(vectorcraft_color::Color::BLACK) } else { self.g.paint.clone() };
|
||||
let style = CharStyle { font_family, font_style, size, fill, stroke: Paint::None, ..CharStyle::default() };
|
||||
let style = CharStyle { font_family, font_style, font_version, size, fill, stroke: Paint::None, ..CharStyle::default() };
|
||||
let mut t = TextObject::point(Point::ZERO, text, style);
|
||||
t.vertical = vertical;
|
||||
let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
|
||||
@@ -31,9 +31,11 @@
|
||||
//! - `ai_editing`: the editing copy of the art a PDF-compatible `.ai` file carries
|
||||
//! ([`export_ai_editing`], [`ai_private_data`]).
|
||||
//! - `ai_ops`: the path and line style operators those two write.
|
||||
//! - [`dcs`]: DCS files, an EPS file with one program per printing plate, read for placing.
|
||||
|
||||
mod ai_editing;
|
||||
mod ai_ops;
|
||||
pub mod dcs;
|
||||
mod import;
|
||||
mod latin;
|
||||
mod legacy_ai;
|
||||
|
||||
@@ -1439,6 +1439,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"",
|
||||
"{set?: index, action?: index, step?: index, command, params?: object} insert the command of a menu item as a step, without running it; the step plays as choosing the item runs it. command and params (default {}) must be those of a menu item that can be a step: one that runs an engine command, whose label does not end with \"…\", outside the VectorCraft, File, Window and Help menus, and that is not Undo, Redo, a Paste, a batch (command.batch), a preference (prefs.*) or action (action.*) command, an Object › Align item, Image Trace's Make or Make and Expand, Pattern's Make or Edit Pattern, a saved selection, saved view or recent font item, or a plug-in's item. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A command and params that no such menu item has, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Menu Item… (dialog `actionInsertMenuItem` with find, command, params, set, action and step) runs this on OK → {set, action, step}, or {recording: true}",
|
||||
),
|
||||
(
|
||||
"ui.action.insertSelectPath",
|
||||
"Insert Select Path",
|
||||
"",
|
||||
"{set?: index, action?: index, step?: index} insert a Select Path step for the path selected in the active document, without changing the selection: the step `select.storedPath {id, fingerprint}` stores the path's id and a fingerprint of its kind, fill rule and anchors, and playback selects only that path while it is still there unchanged and can be selected, else the step is an error. The selection must be one whole path or compound path that can be selected: not anchors or segments alone, several objects, a group, type or another kind of object, a guide, a member of a compound path, or a hidden or locked object. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. No document, a selection that is not such a path, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Select Path runs this, for the selected step or action or the recording in progress → {set, action, step}, or {recording: true}",
|
||||
),
|
||||
(
|
||||
"ui.action.insertSelectObject",
|
||||
"Select Object",
|
||||
|
||||
+39
-12
@@ -81,7 +81,10 @@ pub struct PrintOptions {
|
||||
pub struct PrintReport {
|
||||
pub bytes: Vec<u8>,
|
||||
pub pages: usize,
|
||||
/// What the job leaves out of the document or prints differently.
|
||||
pub warnings: Vec<String>,
|
||||
/// Notes on the options: one the job didn't apply or applied another way.
|
||||
pub setting_notes: Vec<String>,
|
||||
}
|
||||
|
||||
/// One page of a job, as [`preview`] lists it (one copy).
|
||||
@@ -129,7 +132,12 @@ pub struct PrintPreview {
|
||||
pub tiles: Vec<TileGrid>,
|
||||
/// The inks of a separation (empty for composite output).
|
||||
pub inks: Vec<PrintInk>,
|
||||
/// What the job leaves out of the document or prints differently.
|
||||
pub warnings: Vec<String>,
|
||||
/// Notes on the options: one the job doesn't apply or applies another way. They aren't
|
||||
/// serialized: a reply lists them in `warnings`, after what the job leaves out.
|
||||
#[serde(skip)]
|
||||
pub setting_notes: Vec<String>,
|
||||
}
|
||||
|
||||
/// A laid-out job.
|
||||
@@ -144,7 +152,10 @@ struct Job<'a> {
|
||||
plates: Vec<usize>,
|
||||
/// One copy's pages: (layout, plate).
|
||||
sheets: Vec<(usize, Option<usize>)>,
|
||||
/// What the job leaves out of the document or prints differently.
|
||||
warnings: Vec<String>,
|
||||
/// Notes on the options: one the job doesn't apply or applies another way.
|
||||
setting_notes: Vec<String>,
|
||||
}
|
||||
|
||||
/// Keep the layers of `nodes` that print as `which` says, shown (hidden ones print with All),
|
||||
@@ -223,11 +234,11 @@ impl<'a> Job<'a> {
|
||||
fn new(doc: &'a Document, set: &PrintSettings, postscript: bool) -> Result<Self, PdfError> {
|
||||
set.check()?;
|
||||
let doc = printed_document(doc, set);
|
||||
let mut warnings = vec![];
|
||||
let (layouts, tiles) = layout::layout(&doc, set, &mut warnings)?;
|
||||
let (mut warnings, mut setting_notes) = (vec![], vec![]);
|
||||
let (layouts, tiles) = layout::layout(&doc, set, &mut warnings, &mut setting_notes)?;
|
||||
let separations = set.output.mode == OutputMode::Separations;
|
||||
let (inks, more) = if separations { print_inks(&doc, set) } else { Default::default() };
|
||||
warnings.extend(more);
|
||||
setting_notes.extend(more);
|
||||
let plates: Vec<usize> = (0..inks.len()).filter(|i| inks[*i].print).collect();
|
||||
let printed: Vec<Option<usize>> = if separations { (0..plates.len()).map(Some).collect() } else { vec![None] };
|
||||
if printed.is_empty() {
|
||||
@@ -245,18 +256,18 @@ impl<'a> Job<'a> {
|
||||
warnings.push("the document is RGB: separations convert its colours to CMYK with the colour settings".into());
|
||||
}
|
||||
if !postscript && separations && set.output.inks.iter().any(|i| i.frequency.is_some() || i.angle.is_some()) {
|
||||
warnings.push("ink frequencies and angles are not written as halftone screens yet: the output device's screens apply".into());
|
||||
setting_notes.push("ink frequencies and angles are not written as halftone screens yet: the output device's screens apply".into());
|
||||
}
|
||||
if separations && set.advanced.print_as_bitmap {
|
||||
warnings.push("Print as Bitmap is for composite output: separations print the art as it is".into());
|
||||
setting_notes.push("Print as Bitmap is for composite output: separations print the art as it is".into());
|
||||
}
|
||||
if separations && cms::profile(set.color.profile.trim()).is_some_and(|p| p.kind != ProfileKind::Cmyk) {
|
||||
warnings.push("separations separate with a CMYK profile: the RGB printer profile is left out".into());
|
||||
setting_notes.push("separations separate with a CMYK profile: the RGB printer profile is left out".into());
|
||||
}
|
||||
if !postscript && !set.graphics.auto_flatness {
|
||||
warnings.push("a fixed flatness is not written yet: the output device flattens curves".into());
|
||||
setting_notes.push("a fixed flatness is not written yet: the output device flattens curves".into());
|
||||
}
|
||||
Ok(Self { doc, layouts, tiles, inks, plates, sheets, warnings })
|
||||
Ok(Self { doc, layouts, tiles, inks, plates, sheets, warnings, setting_notes })
|
||||
}
|
||||
|
||||
/// The ink of plate `plate` (`None`: composite).
|
||||
@@ -303,7 +314,14 @@ pub fn preview(doc: &Document, set: &PrintSettings) -> Result<PrintPreview, PdfE
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(PrintPreview { pages: job.sheets.len() * set.copies.max(1) as usize, sheets, tiles: job.tiles, inks: job.inks, warnings: job.warnings })
|
||||
Ok(PrintPreview {
|
||||
pages: job.sheets.len() * set.copies.max(1) as usize,
|
||||
sheets,
|
||||
tiles: job.tiles,
|
||||
inks: job.inks,
|
||||
warnings: job.warnings,
|
||||
setting_notes: job.setting_notes,
|
||||
})
|
||||
}
|
||||
|
||||
/// One sheet of a laid-out job, ready for a writer (PDF here, PostScript in the EPS writer).
|
||||
@@ -371,7 +389,10 @@ pub struct PrintPlan<'a> {
|
||||
pub title: String,
|
||||
/// When the job ran (Unix seconds, UTC).
|
||||
pub created: Option<i64>,
|
||||
/// What the job leaves out of the document or prints differently.
|
||||
pub warnings: Vec<String>,
|
||||
/// Notes on the options: one the job doesn't apply or applies another way.
|
||||
pub setting_notes: Vec<String>,
|
||||
}
|
||||
|
||||
/// Lay out printing `doc` as `opts` say, for a writer to draw.
|
||||
@@ -415,7 +436,7 @@ pub fn plan<'a>(doc: &'a Document, opts: &PrintOptions) -> Result<PrintPlan<'a>,
|
||||
})
|
||||
.collect();
|
||||
let order = job.order(set);
|
||||
Ok(PrintPlan { plates, sheets, order, title, created, warnings: job.warnings, doc: job.doc })
|
||||
Ok(PrintPlan { plates, sheets, order, title, created, warnings: job.warnings, setting_notes: job.setting_notes, doc: job.doc })
|
||||
}
|
||||
|
||||
/// Landscape sheet `s` upright on the paper ([`PrintOptions::for_printer`]): a page wider than
|
||||
@@ -473,12 +494,18 @@ fn print_inner(doc: &Document, opts: &PrintOptions, left_out: &mut HashSet<Strin
|
||||
let (bytes, more, settings) = w.finish(left_out, false)?;
|
||||
let bytes = crate::forms::finish(bytes, doc, &pdf, false, overprinted, invisible)?;
|
||||
let mut warnings = plan.warnings;
|
||||
for m in more.into_iter().chain(settings) {
|
||||
for m in more {
|
||||
if !warnings.contains(&m) {
|
||||
warnings.push(m);
|
||||
}
|
||||
}
|
||||
Ok(PrintReport { bytes, pages: plan.order.len(), warnings })
|
||||
let mut setting_notes: Vec<String> = vec![];
|
||||
for m in plan.setting_notes.into_iter().chain(settings) {
|
||||
if !warnings.contains(&m) && !setting_notes.contains(&m) {
|
||||
setting_notes.push(m);
|
||||
}
|
||||
}
|
||||
Ok(PrintReport { bytes, pages: plan.order.len(), warnings, setting_notes })
|
||||
}
|
||||
|
||||
/// The printer's marks of page `l` (`None` without marks), with its page information: the
|
||||
|
||||
@@ -184,8 +184,14 @@ fn too_many_pages() -> PdfError {
|
||||
}
|
||||
|
||||
/// The pages of `doc` (whose layers are those that print) as `set` lays them out, the tiles of
|
||||
/// each artboard when tiling, and warnings (art that doesn't fit the imageable area).
|
||||
pub(crate) fn layout(doc: &Document, set: &PrintSettings, warnings: &mut Vec<String>) -> Result<(Vec<Layout>, Vec<TileGrid>), PdfError> {
|
||||
/// each artboard when tiling. `warnings` gets what doesn't print (art outside the imageable area,
|
||||
/// an artboard with no area), and `setting_notes` the copies per sheet that don't fit.
|
||||
pub(crate) fn layout(
|
||||
doc: &Document,
|
||||
set: &PrintSettings,
|
||||
warnings: &mut Vec<String>,
|
||||
setting_notes: &mut Vec<String>,
|
||||
) -> Result<(Vec<Layout>, Vec<TileGrid>), PdfError> {
|
||||
let marks = set.marks.printer_marks();
|
||||
let bleed = set.bleed.of(doc);
|
||||
let (pw, ph) = set.paper();
|
||||
@@ -278,7 +284,7 @@ pub(crate) fn layout(doc: &Document, set: &PrintSettings, warnings: &mut Vec<Str
|
||||
let fit = set.copies_per_sheet.then(|| (fitting(imageable.width(), extent.0, gutter), fitting(imageable.height(), extent.1, gutter)));
|
||||
let grid = match fit {
|
||||
Some((cols, rows)) if cols < 1.0 || rows < 1.0 => {
|
||||
warnings.push(format!("{what} with its bleed and marks doesn't fit the imageable area: it prints one copy per sheet"));
|
||||
setting_notes.push(format!("{what} with its bleed and marks doesn't fit the imageable area: it prints one copy per sheet"));
|
||||
None
|
||||
}
|
||||
Some((cols, rows)) if cols * rows > MAX_COPIES_PER_SHEET as f64 => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Separations: the inks of a job ([`print_inks`]) and the document as one ink's plate, every
|
||||
//! colour (and image pixel) replaced by the grey of that ink's coverage.
|
||||
//! colour (and image pixel) replaced by the grey of that ink's coverage. An image placed from a
|
||||
//! DCS file prints its own plates ([`vectorcraft_doc::separated`]).
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::Arc;
|
||||
@@ -130,7 +131,30 @@ impl<'a> Separator<'a> {
|
||||
kept.insert(colorized_key(&key), blob.clone());
|
||||
}
|
||||
}
|
||||
// A DCS file's image prints its own plates: a spot plate on its ink when the job prints
|
||||
// that ink, else as its process equivalent.
|
||||
let spots: BTreeSet<String> = if self.set.output.spots_to_process {
|
||||
BTreeSet::new()
|
||||
} else {
|
||||
plates(self.doc).into_iter().filter(|p| p.spot).map(|p| p.name).collect()
|
||||
};
|
||||
let own = |name: &str| spots.contains(name);
|
||||
// A spot plate printing as process prints as the document's swatch of its ink does, when
|
||||
// there is one; else as the process equivalent its file gives.
|
||||
let equivalent = |p: &vectorcraft_doc::separated::InkPlate| match self.doc.swatch(&p.name).and_then(|w| Some((w.paint.color()?, w.spot))) {
|
||||
Some((c, spot)) => {
|
||||
let c = self.doc.linked_color(c, spot);
|
||||
cms::PROCESS_PLATES.map(|q| self.ink(q, &c, None))
|
||||
}
|
||||
None => p.cmyk,
|
||||
};
|
||||
for blob in d.images.values_mut() {
|
||||
if let Some(s) = blob.separated() {
|
||||
if let Some(gray) = s.plate_png(plate, &own, &equivalent) {
|
||||
*blob = gray;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let gray = blob.map_rgb(|[r, g, b]| {
|
||||
let rgb = |v: u8| v as f32 / 255.0;
|
||||
let v = (255.0 * (1.0 - self.ink(plate, &Color::rgb(rgb(r), rgb(g), rgb(b)), None))).round() as u8;
|
||||
|
||||
@@ -322,6 +322,76 @@ fn image_doc(blob: ImageBlob, w: u32, h: u32) -> Document {
|
||||
d
|
||||
}
|
||||
|
||||
/// An image placed from a DCS file prints each of its plates on its own ink: a spot plate on its
|
||||
/// spot ink's plate, or as its process equivalent when spots print as process. Its composite, the
|
||||
/// CMYK samples every other reader sees, never prints on a separation.
|
||||
#[test]
|
||||
fn a_dcs_image_prints_each_plate_on_its_own_ink() {
|
||||
use vectorcraft_doc::separated::{InkPlate, Separated};
|
||||
let plate = |name: &str, cmyk: [f32; 4], data: [u8; 2]| InkPlate { name: name.into(), cmyk, data: data.to_vec() };
|
||||
let s = Separated::new(2, 1, vec![plate("Cyan", [0.0; 4], [255, 0]), plate("Spot Orange", [0.0, 0.6, 1.0, 0.0], [128, 0])]).unwrap();
|
||||
let mut d = image_doc(ImageBlob::separated_tiff(&s, None, &[]).unwrap(), 2, 1);
|
||||
d.swatches.push(Swatch { name: "Spot Orange".into(), paint: Paint::solid(Color::cmyk(0.0, 0.6, 1.0, 0.0)), global: true, spot: true });
|
||||
let plates = |spots_to_process: bool| {
|
||||
let settings: PrintSettings = serde_json::from_value(json!({"output": {"mode": "separations", "spotsToProcess": spots_to_process}})).unwrap();
|
||||
let names: Vec<String> = print_inks(&d, &settings).0.into_iter().map(|i| i.name).collect();
|
||||
let job = plan(&d, &PrintOptions { settings, ..Default::default() }).unwrap();
|
||||
let coverage: Vec<Vec<u8>> = job
|
||||
.plates
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let NodeKind::Image(im) = &p.layers[0].children().unwrap()[0].kind else { panic!("an image") };
|
||||
p.image_blob(im).and_then(|b| b.coverage()).unwrap().2
|
||||
})
|
||||
.collect();
|
||||
names.into_iter().zip(coverage).collect::<Vec<_>>()
|
||||
};
|
||||
let own = plates(false);
|
||||
let get = |all: &[(String, Vec<u8>)], name: &str| all.iter().find(|(n, _)| n == name).map(|(_, c)| c.clone());
|
||||
assert_eq!(get(&own, "Cyan"), Some(vec![255, 0]));
|
||||
assert_eq!(get(&own, "Magenta"), Some(vec![0, 0]), "the spot ink stays off the process plates");
|
||||
assert_eq!(get(&own, "Yellow"), Some(vec![0, 0]));
|
||||
assert_eq!(get(&own, "Spot Orange"), Some(vec![128, 0]));
|
||||
let process = plates(true);
|
||||
assert_eq!(get(&process, "Spot Orange"), None);
|
||||
assert_eq!(get(&process, "Magenta"), Some(vec![77, 0]), "60% of the spot plate's 50%");
|
||||
assert_eq!(get(&process, "Yellow"), Some(vec![128, 0]));
|
||||
}
|
||||
|
||||
/// A DCS spot plate that prints as process prints as the document's swatch of its ink does (the
|
||||
/// swatch kept in place of the plate's), not as the equivalent its file gives.
|
||||
#[test]
|
||||
fn a_dcs_spot_plate_printing_as_process_takes_the_documents_swatch() {
|
||||
use vectorcraft_doc::separated::{InkPlate, Separated};
|
||||
let plate = |name: &str, cmyk: [f32; 4], data: [u8; 2]| InkPlate { name: name.into(), cmyk, data: data.to_vec() };
|
||||
let s = Separated::new(2, 1, vec![plate("Cyan", [0.0; 4], [255, 0]), plate("Spot Orange", [0.0, 0.6, 1.0, 0.0], [0, 128])]).unwrap();
|
||||
let mut d = image_doc(ImageBlob::separated_tiff(&s, None, &[]).unwrap(), 2, 1);
|
||||
let plates = |d: &Document, spots_to_process: bool| {
|
||||
let settings: PrintSettings = serde_json::from_value(json!({"output": {"mode": "separations", "spotsToProcess": spots_to_process}})).unwrap();
|
||||
let names: Vec<String> = print_inks(d, &settings).0.into_iter().map(|i| i.name).collect();
|
||||
let job = plan(d, &PrintOptions { settings, ..Default::default() }).unwrap();
|
||||
let coverage = job.plates.iter().map(|p| {
|
||||
let NodeKind::Image(im) = &p.layers[0].children().unwrap()[0].kind else { panic!("an image") };
|
||||
p.image_blob(im).and_then(|b| b.coverage()).unwrap().2
|
||||
});
|
||||
names.into_iter().zip(coverage).collect::<Vec<_>>()
|
||||
};
|
||||
let get = |all: &[(String, Vec<u8>)], name: &str| all.iter().find(|(n, _)| n == name).map(|(_, c)| c.clone());
|
||||
// A spot swatch of the ink in another color.
|
||||
d.swatches.push(Swatch { name: "Spot Orange".into(), paint: Paint::solid(Color::cmyk(1.0, 0.0, 0.0, 0.0)), global: true, spot: true });
|
||||
let process = plates(&d, true);
|
||||
assert_eq!(get(&process, "Cyan"), Some(vec![255, 128]), "the swatch's cyan");
|
||||
assert_eq!(get(&process, "Magenta"), Some(vec![0, 0]), "not the file's magenta");
|
||||
assert_eq!(get(&process, "Yellow"), Some(vec![0, 0]));
|
||||
// A process swatch of the ink's name: the job prints no plate for it.
|
||||
d.swatches.retain(|w| w.name != "Spot Orange");
|
||||
d.swatches.push(Swatch { name: "Spot Orange".into(), paint: Paint::solid(Color::cmyk(0.0, 0.0, 0.0, 1.0)), global: true, spot: false });
|
||||
let own = plates(&d, false);
|
||||
assert_eq!(get(&own, "Spot Orange"), None);
|
||||
assert_eq!(get(&own, "Black"), Some(vec![0, 128]), "the swatch's black");
|
||||
assert_eq!(get(&own, "Yellow"), Some(vec![0, 0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_output_settings_convert_cmyk_images_and_compression_recompresses_them_in_cmyk() {
|
||||
let ink = [0u8, 102, 255, 0];
|
||||
|
||||
@@ -275,11 +275,8 @@ fn an_artboard_that_does_not_fit_prints_one_copy_per_sheet() {
|
||||
let d = doc(700.0, 900.0, &[(Rect::new(0.0, 0.0, 700.0, 900.0), rgb(0.0, 0.0, 1.0))]);
|
||||
let r = job(&d, json!({"copiesPerSheet": true}));
|
||||
assert_eq!(r.pages, 1);
|
||||
assert!(
|
||||
r.warnings.iter().any(|w| w == "Artboard 1 with its bleed and marks doesn't fit the imageable area: it prints one copy per sheet"),
|
||||
"{:?}",
|
||||
r.warnings
|
||||
);
|
||||
// A note on the options; the art that doesn't print is what the job loses.
|
||||
assert_eq!(r.setting_notes, ["Artboard 1 with its bleed and marks doesn't fit the imageable area: it prints one copy per sheet"]);
|
||||
assert!(r.warnings.iter().any(|w| w.contains("larger than the imageable area")), "{:?}", r.warnings);
|
||||
assert_eq!(r.bytes, job(&d, json!({})).bytes, "printed as without copies per sheet");
|
||||
assert!(preview(&d, &settings(json!({"copiesPerSheet": true}))).unwrap().sheets[0].copies.is_empty());
|
||||
|
||||
@@ -136,5 +136,5 @@ fn separations_separate_with_a_cmyk_printer_profile() {
|
||||
assert_ne!(device, press, "the profile separates");
|
||||
// An RGB printer profile doesn't separate.
|
||||
let r = job(&d, json!({"output": {"mode": "separations"}, "color": {"profile": cms::SRGB}}));
|
||||
assert!(r.warnings.iter().any(|w| w.contains("RGB printer profile")), "{:?}", r.warnings);
|
||||
assert!(r.setting_notes.iter().any(|w| w.contains("RGB printer profile")), "{:?}", r.setting_notes);
|
||||
}
|
||||
|
||||
@@ -976,7 +976,10 @@ fn dithered_type_keeps_its_resolution_when_scaled_and_stays_searchable() {
|
||||
|
||||
/// Exact craft-fonts face: a substituted Arabic font would miss Noto's dot decomposition.
|
||||
fn mark_text(text: &str) -> Option<TextObject> {
|
||||
let face = FontDb::global().face("Noto Sans Arabic", "Regular").filter(|f| f.family == "Noto Sans Arabic");
|
||||
// The craft-fonts build of the face: a system font of the same family (Linux distributions
|
||||
// install one) can lack the Latin and digit glyphs these fixtures shape with it.
|
||||
let bundled = vectorcraft_text::CRAFT_FONTS.iter().any(|f| f.family == "Noto Sans Arabic" && f.style == "Regular");
|
||||
let face = FontDb::global().face("Noto Sans Arabic", "Regular").filter(|f| bundled && f.family == "Noto Sans Arabic");
|
||||
let Some(face) = face else {
|
||||
assert!(std::env::var("CRAFT_FONTS_REQUIRED").as_deref() != Ok("1"), "Noto Sans Arabic is required");
|
||||
eprintln!("SKIPPED: PDF mark-text regression needs craft-fonts Noto Sans Arabic");
|
||||
|
||||
@@ -54,13 +54,15 @@ pub(super) fn evaluates_live(n: &Node) -> bool {
|
||||
/// through live objects it evaluates? It can when `n` holds type (laid out in the fonts installed),
|
||||
/// a symbol instance (made from its symbol's art), an object with a 3D effect or a color
|
||||
/// adjustment (shaded or recolored through the color settings), one with Vector Halftone (its
|
||||
/// dots sampled from everything it draws), or one with a live-effect plug-in that paints its pieces
|
||||
/// (drawn only while the swatches and patterns they name are in the document), in it or in the
|
||||
/// opacity masks of the objects in it.
|
||||
/// dots sampled from everything it draws), one with a live-effect plug-in that paints its pieces
|
||||
/// (drawn only while the swatches and patterns they name are in the document), or an image with a
|
||||
/// fill or stroke that follows the outline of its pixels ([`effects::follows_image_pixels`]: the
|
||||
/// document's pixels for it can change), in it or in the opacity masks of the objects in it.
|
||||
pub(super) fn reads_outside(n: &Node) -> bool {
|
||||
let mut reads = false;
|
||||
n.walk(&mut |c| {
|
||||
reads |= matches!(c.kind, NodeKind::Text(_) | NodeKind::SymbolInstance { .. })
|
||||
|| effects::follows_image_pixels(c)
|
||||
|| effects::has_revolve(c)
|
||||
|| effects::has_solid(c)
|
||||
|| effects::has_adjustment(c)
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
//! envelope around an instance of it). What the walk behind that key found is kept with the node
|
||||
//! and with the document's symbols and patterns as the walk read them ([`Walk`]). A later frame
|
||||
//! makes the key from what was kept while the node and the document's symbols and patterns stay
|
||||
//! the same. A walk that found a placed file is not kept.
|
||||
//! the same. A walk that found a placed file, or an image whose own fills or strokes follow its
|
||||
//! pixels, is not kept.
|
||||
//! Gradient meshes are tessellated into many small solid-colour quads (vello
|
||||
//! has no mesh shading), with the density chosen from the on-screen patch size; each quad is
|
||||
//! grown by half a device pixel along its edges so neighbours overlap and no antialiasing seams
|
||||
@@ -44,7 +45,8 @@ type Walk = (Weak<Node>, Walked, Arc<DocInputs>);
|
||||
|
||||
/// Per-renderer cache of evaluated live objects: (live node, its evaluation, [`Found`]), a
|
||||
/// [`MeshEntry`] per mesh node and subdivision level, and a [`Walk`] per node whose key
|
||||
/// [`Renderer::live_found`] found without finding a placed file.
|
||||
/// [`Renderer::live_found`] found without finding a placed file or an image whose own fills or
|
||||
/// strokes follow its pixels.
|
||||
pub(crate) struct LiveCache {
|
||||
expanded: FrameCache<usize, (Arc<Node>, Expanded, Found)>,
|
||||
meshes: FrameCache<(usize, usize), MeshEntry>,
|
||||
@@ -97,7 +99,8 @@ pub fn expand_live(n: &Node) -> Vec<Node> {
|
||||
/// What the evaluation of `n` (a live node, or an object whose effect art the renderer keeps with
|
||||
/// its inputs, see [`Renderer::live_found`]) reads from the document, placed files,
|
||||
/// the fonts, the plug-ins and the color settings, hashed: the art of the symbols, the tilings of
|
||||
/// the patterns and the reads of the placed files it shows ([`hash_resources`]), the font
|
||||
/// the patterns, the reads of the placed files it shows and the document's pixels for the images
|
||||
/// whose own fills or strokes follow them ([`hash_resources`]), the font
|
||||
/// generation ([`vectorcraft_text::FontDb::generation`]) when it shows type or a placed file, the
|
||||
/// plug-in revision ([`crate::effects::plugin_revision`]) and `names` (the hash of the document's
|
||||
/// swatch and pattern names, [`DocInputs`]) when it shows an object with a plug-in effect, since a
|
||||
@@ -105,13 +108,13 @@ pub fn expand_live(n: &Node) -> Vec<Node> {
|
||||
/// while the swatches and patterns they name are in the document, and the color settings (`cms`,
|
||||
/// their address). Art is hashed by the addresses of its `Arc`s, which copies of the document
|
||||
/// share: a copy of the document has the same key. It also returns weak references to the reads
|
||||
/// found, and what the walk found ([`Walked`]) when it found no placed file.
|
||||
/// found, and what the walk found ([`Walked`]) when it found no placed file and no such image.
|
||||
fn resources_key(doc: &Document, cms: usize, names: u64, n: &Node) -> (u64, Vec<Weak<ReadDocument>>, Option<Walked>) {
|
||||
let mut h = DefaultHasher::new();
|
||||
let mut shown = Shown::default();
|
||||
hash_resources(doc, n, &mut shown, &mut h);
|
||||
let walked = Walked { hash: h.finish(), text: shown.text, plugins: shown.plugins };
|
||||
(walked.key(cms, names), shown.reads, (!shown.placed).then_some(walked))
|
||||
(walked.key(cms, names), shown.reads, (!shown.placed && !shown.pixels).then_some(walked))
|
||||
}
|
||||
|
||||
/// What [`hash_resources`] found in a node: the hash of what it walked, whether it found type or
|
||||
@@ -153,6 +156,10 @@ struct Shown {
|
||||
text: bool,
|
||||
/// Whether a placed file was found.
|
||||
placed: bool,
|
||||
/// Whether an image whose own fills or strokes follow its pixels was found
|
||||
/// ([`crate::effects::follows_image_pixels`]): the document's pixels for it can change while
|
||||
/// the walk's symbols and patterns stay the same.
|
||||
pixels: bool,
|
||||
/// Whether an object with a visible plug-in effect was found.
|
||||
plugins: bool,
|
||||
/// Weak references to the reads of the placed files found.
|
||||
@@ -163,9 +170,11 @@ struct Shown {
|
||||
/// tilings ([`crate::pattern::hash_tiling`]) of the patterns in the fills and strokes of its
|
||||
/// objects and of type's characters, the address of each symbol instance's art, and the address of
|
||||
/// the read of each placed file that [`vectorcraft_doc::PlacedDocument::art`] shows (none before
|
||||
/// the file is read). It walks `n` and then, one at a time from a list, the opacity-mask, pattern
|
||||
/// and symbol art it finds, each pattern and each symbol's art once ([`first_seen`]). `shown`
|
||||
/// records the type, placed files, reads and plug-in effects found.
|
||||
/// the file is read), and the address of the document's pixels for each image whose
|
||||
/// own fills or strokes follow them ([`crate::effects::follows_image_pixels`]). It walks `n` and
|
||||
/// then, one at a time from a list, the opacity-mask, pattern and symbol art it finds, each pattern
|
||||
/// and each symbol's art once ([`first_seen`]). `shown` records the type, placed files, reads,
|
||||
/// such images and plug-in effects found.
|
||||
fn hash_resources<'a>(doc: &'a Document, n: &'a Node, shown: &mut Shown, h: &mut DefaultHasher) {
|
||||
let mut todo = vec![];
|
||||
let mut next = Some(n);
|
||||
@@ -203,6 +212,12 @@ fn hash_resources<'a>(doc: &'a Document, n: &'a Node, shown: &mut Shown, h: &mut
|
||||
shown.placed = true;
|
||||
}
|
||||
NodeKind::Text(_) => shown.text = true,
|
||||
// The address of the pixels the document holds for the image, which Update Link
|
||||
// replaces under the same key ([`DocInputs`] holds them weakly).
|
||||
NodeKind::Image(im) if crate::effects::follows_image_pixels(c) => {
|
||||
doc.images.get(&im.key).map(|b| Arc::as_ptr(&b.bytes) as usize).hash(h);
|
||||
shown.pixels = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(m) = c.extras().mask.as_deref() {
|
||||
@@ -442,7 +457,7 @@ impl Renderer {
|
||||
/// evaluated live objects, type, symbol art or colors shaded through the color settings. The
|
||||
/// key is made from the [`Walk`] kept for `n` while `doc`'s symbols and patterns are those it
|
||||
/// read; `n` is walked again when they changed, and in every frame when the walk found a
|
||||
/// placed file.
|
||||
/// placed file or an image whose own fills or strokes follow its pixels.
|
||||
pub(crate) fn live_found(&mut self, doc: &Document, n: &Arc<Node>) -> Found {
|
||||
let inputs = self.doc_inputs(doc);
|
||||
let at = Arc::as_ptr(n) as usize;
|
||||
|
||||
+136
-6
@@ -10,7 +10,8 @@
|
||||
//! Registration swatch print their tint on every plate, spot plates included.
|
||||
//! Placed raster images show their pixels' process inks the same way ([`plate_image`]): a CMYK
|
||||
//! image its own inks, an RGB one its pixels separated through the color settings. They carry
|
||||
//! no spot ink; a colorized image takes its color's plates, as a fill of it does.
|
||||
//! no spot ink; a colorized image takes its color's plates, as a fill of it does. An image
|
||||
//! placed from a DCS file shows its own plates, spot plates included ([`plates_shown`]).
|
||||
//! * **Overprint Preview** (and Separations Preview, which implies it): fills and strokes that
|
||||
//! overprint ([`vectorcraft_doc::FillLayer::overprint`], characters' too) are drawn with
|
||||
//! Darken, which approximates their inks printing over the inks below: a zero ink lets the
|
||||
@@ -82,6 +83,10 @@ fn plate_color(doc: &Document, c: &Cms, proof: &ProofSetup, visible: &[String],
|
||||
/// profiles. Each table is made when an image first needs it.
|
||||
pub(crate) struct PlateLut {
|
||||
proof: ProofSetup,
|
||||
/// The document's solid swatches: name, whether it is a spot color, and its color. An image
|
||||
/// placed from a DCS file shows a spot plate on its own ink only when it is one of the spot
|
||||
/// ones, in that swatch's color, and else as the swatch of its name ([`plates_shown`]).
|
||||
swatches: Vec<(String, bool, Color)>,
|
||||
cms: Arc<Cms>,
|
||||
/// [`cms::profiles_generation`].
|
||||
profiles: u64,
|
||||
@@ -107,20 +112,29 @@ static PLATE_LUTS: Mutex<Vec<Arc<PlateLut>>> = Mutex::new(Vec::new());
|
||||
/// The last [`PlateLut::generation`] handed out.
|
||||
static PLATE_GENERATION: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// The tables placed images show Separations Preview's visible plates through with `opts`, or
|
||||
/// `None` when it is off.
|
||||
pub(crate) fn plate_lut(opts: &RenderOptions) -> Option<Arc<PlateLut>> {
|
||||
/// The tables the placed images of `doc` show Separations Preview's visible plates through with
|
||||
/// `opts`, or `None` when it is off.
|
||||
pub(crate) fn plate_lut(opts: &RenderOptions, doc: &Document) -> Option<Arc<PlateLut>> {
|
||||
let proof = opts.proof.as_ref().filter(|p| p.separations.is_some())?;
|
||||
let (c, profiles) = (cms::active(), cms::profiles_generation());
|
||||
// The spot ones are the spot plates of `vectorcraft_doc::inks::plates`; their colors are part
|
||||
// of the tables' identity, so a swatch edit shows.
|
||||
let swatches: Vec<(String, bool, Color)> = doc
|
||||
.swatches_iter()
|
||||
.filter_map(|s| match &s.paint {
|
||||
vectorcraft_color::Paint::Solid { color, .. } => Some((s.name.clone(), s.spot, doc.linked_color(*color, s.spot))),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let mut luts = PLATE_LUTS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(l) = luts.iter().find(|l| Arc::ptr_eq(&l.cms, &c) && l.profiles == profiles && l.proof == *proof) {
|
||||
if let Some(l) = luts.iter().find(|l| Arc::ptr_eq(&l.cms, &c) && l.profiles == profiles && l.proof == *proof && l.swatches == swatches) {
|
||||
return Some(l.clone());
|
||||
}
|
||||
if luts.len() >= 4 {
|
||||
luts.remove(0);
|
||||
}
|
||||
let generation = PLATE_GENERATION.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
let l = Arc::new(PlateLut { proof: proof.clone(), cms: c, profiles, generation, rgb: OnceLock::new(), cmyk: OnceLock::new() });
|
||||
let l = Arc::new(PlateLut { proof: proof.clone(), swatches, cms: c, profiles, generation, rgb: OnceLock::new(), cmyk: OnceLock::new() });
|
||||
luts.push(l.clone());
|
||||
Some(l)
|
||||
}
|
||||
@@ -134,6 +148,11 @@ pub(crate) fn plate_image(lut: &PlateLut, key: &ImageKey, pm: &Pixmap, blob: Opt
|
||||
if let Some(p) = lookup(&want, touch) {
|
||||
return (want, p);
|
||||
}
|
||||
// An image placed from a DCS file shows its own plates, spot plates included.
|
||||
if let Some(s) = blob.filter(|b| crate::ink::shows_own_inks(key) && b.is_separated()).and_then(ImageBlob::separated) {
|
||||
let out = images::store().insert(want.clone(), Arc::new(plates_shown(lut, &s, pm)));
|
||||
return (want, out);
|
||||
}
|
||||
let size = (u32::from(pm.width()), u32::from(pm.height()));
|
||||
let raw = crate::ink::key_inks(key, blob).filter(|i| (i.width, i.height) == size);
|
||||
let mut out = pm.clone();
|
||||
@@ -165,6 +184,117 @@ pub(crate) fn plate_image(lut: &PlateLut, key: &ImageKey, pm: &Pixmap, blob: Opt
|
||||
(want, out)
|
||||
}
|
||||
|
||||
/// The visible plates of separated image `s` drawn at the size of `pm` (each pixel the mean of the
|
||||
/// plate pixels it covers, [`Fit`]): with one spot plate visible its ink as a grey, else the
|
||||
/// visible process inks through `lut` with the visible spot inks multiplied on top, each in the
|
||||
/// color of the document's spot swatch of its ink. A spot plate whose ink the document has no spot
|
||||
/// swatch for shows on the process plates as it prints
|
||||
/// ([`vectorcraft_doc::separated::Separated::coverage_as`]): as the document's swatch of its name
|
||||
/// when there is one, else as the process equivalent its file gives.
|
||||
fn plates_shown(lut: &PlateLut, s: &vectorcraft_doc::separated::Separated, pm: &Pixmap) -> Pixmap {
|
||||
let visible = lut.proof.separations.as_deref().unwrap_or_default();
|
||||
let shown = |name: &str| visible.iter().any(|v| v == name);
|
||||
let swatch = |name: &str| lut.swatches.iter().find(|(n, ..)| n == name);
|
||||
let own = |name: &str| swatch(name).is_some_and(|(_, spot, _)| *spot);
|
||||
let (w, h) = (usize::from(pm.width()).max(1), usize::from(pm.height()).max(1));
|
||||
let fit = Fit::new(((s.width as usize).max(1), (s.height as usize).max(1)), (w, h));
|
||||
let alone = match visible {
|
||||
[one] if !PROCESS_PLATES.contains(&one.as_str()) => Some(s.plates.iter().find(|p| p.name == *one && own(&p.name))),
|
||||
_ => None,
|
||||
};
|
||||
let spots: Vec<(&[u8], [f32; 3])> = s
|
||||
.spots()
|
||||
.filter(|p| own(&p.name) && shown(&p.name))
|
||||
.map(|p| (p.data.as_slice(), swatch(&p.name).map_or_else(|| lut.cms.cmyk_to_srgb(p.cmyk, false), |(.., c)| lut.cms.display_rgb(c))))
|
||||
.collect();
|
||||
// The plates that print on the process plates, each with the process inks it stands for.
|
||||
let process: Vec<(&[u8], [f32; 4])> = s
|
||||
.plates
|
||||
.iter()
|
||||
.filter(|p| !p.spot() || !own(&p.name))
|
||||
.map(|p| {
|
||||
let inks = match swatch(&p.name) {
|
||||
Some((.., c)) if p.spot() => lut.cms.to_cmyk(c, lut.proof.intent),
|
||||
_ => p.cmyk,
|
||||
};
|
||||
(p.data.as_slice(), inks)
|
||||
})
|
||||
.collect();
|
||||
let t = lut.cmyk.get_or_init(|| {
|
||||
CmykLut::build(|inks| {
|
||||
let [x, y, z] = lab::srgb_to_xyz(lut.shown(inks));
|
||||
[x, y, z, 0.0]
|
||||
})
|
||||
});
|
||||
let mut out = pm.clone();
|
||||
for (at, o) in out.data_mut().iter_mut().enumerate() {
|
||||
let ink = |data: &[u8]| fit.ink(data, at);
|
||||
let rgb = match alone {
|
||||
Some(p) => [1.0 - p.map_or(0.0, |p| ink(&p.data)); 3],
|
||||
None => {
|
||||
let mut clear = [1.0f32; 4];
|
||||
for (data, inks) in &process {
|
||||
let a = ink(data);
|
||||
for (c, e) in clear.iter_mut().zip(inks) {
|
||||
*c *= 1.0 - a * e;
|
||||
}
|
||||
}
|
||||
let [x, y, z, _] = t.apply(clear.map(|c| 1.0 - c));
|
||||
let mut rgb = lab::xyz_to_srgb([x, y, z]);
|
||||
for (data, c) in &spots {
|
||||
let a = ink(data);
|
||||
for (v, c) in rgb.iter_mut().zip(c) {
|
||||
*v *= 1.0 - a * (1.0 - c);
|
||||
}
|
||||
}
|
||||
rgb
|
||||
}
|
||||
};
|
||||
let [r, g, b] = rgb.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8);
|
||||
*o = PremulRgba8 { r, g, b, a: 255 };
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Plates of `from` (width, height) pixels drawn at `to` pixels: each pixel the mean of the plate
|
||||
/// pixels it covers, so a reduced image (a smaller mip level) keeps the coverage the plate has; the
|
||||
/// nearest plate pixel when it covers less than one. It keeps only the spans of each column and
|
||||
/// row, not a copy of any plate.
|
||||
struct Fit {
|
||||
/// The plate's width.
|
||||
sw: usize,
|
||||
/// The shown width.
|
||||
w: usize,
|
||||
/// The plate columns each shown column covers, `start..end`.
|
||||
columns: Vec<(usize, usize)>,
|
||||
/// The plate rows each shown row covers, `start..end`.
|
||||
rows: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl Fit {
|
||||
fn new((sw, sh): (usize, usize), (w, h): (usize, usize)) -> Self {
|
||||
// The plate pixels each shown column or row covers: at least one.
|
||||
// In 64 bits: a plate's width times the shown width may not fit 32 (the web build).
|
||||
let span = |i: usize, n: usize, of: usize| {
|
||||
let at = |i: usize| usize::try_from(i as u64 * of as u64 / n as u64).unwrap_or(of);
|
||||
let start = at(i).min(of - 1);
|
||||
(start, at(i + 1).clamp(start + 1, of))
|
||||
};
|
||||
Self { sw, w, columns: (0..w).map(|x| span(x, w, sw)).collect(), rows: (0..h).map(|y| span(y, h, sh)).collect() }
|
||||
}
|
||||
|
||||
/// The ink of plate `data` at shown pixel `at` (row by row), from 0 to 1.
|
||||
fn ink(&self, data: &[u8], at: usize) -> f32 {
|
||||
let (Some(&(x0, x1)), Some(&(y0, y1))) = (self.columns.get(at % self.w), self.rows.get(at / self.w)) else { return 0.0 };
|
||||
let mut sum = 0u64;
|
||||
for row in y0..y1 {
|
||||
sum += data.get(row * self.sw + x0..row * self.sw + x1).map_or(0, |r| r.iter().map(|&v| u64::from(v)).sum());
|
||||
}
|
||||
let n = ((y1 - y0) * (x1 - x0)) as u64;
|
||||
sum as f32 / n as f32 / 255.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The art a renderer drew with Overprint Preview or Separations Preview, kept for its next frame
|
||||
/// ([`prepare`]): the document's layers and symbol art as they were, each beside the art drawn for
|
||||
/// it, and what that art was made with. The kept nodes keep their addresses: no other node gets
|
||||
|
||||
@@ -150,7 +150,7 @@ impl Renderer {
|
||||
let screen = if opts.rich_black { Ink::RichBlack } else { Ink::Display };
|
||||
let symbols = doc.symbol_boxes().quick();
|
||||
let output = Affine::scale(1.0 / f64::from(cell.max(1))) * view;
|
||||
let plates = proof::plate_lut(opts);
|
||||
let plates = proof::plate_lut(opts, doc);
|
||||
let frame = Frame { mt: threads > 0, doc, symbols: &symbols, view, visible, px, opts, ink: screen, output, plates: plates.as_deref() };
|
||||
self.begin_frame();
|
||||
// View Opacity Mask: the mask's coverage in greyscale instead of the artwork.
|
||||
|
||||
@@ -182,6 +182,113 @@ fn separations_preview_shows_the_image_on_its_spot_plate() {
|
||||
assert!(off[..3].iter().all(|v| *v >= 250), "the process plates show paper, got {off:?}");
|
||||
}
|
||||
|
||||
/// Separations Preview shows an image placed from a DCS file through its own plates: its spot
|
||||
/// plate on that ink's plate only, its cyan plate on the cyan plate only. Without the preview it
|
||||
/// shows its composite, the spot ink mixed in.
|
||||
#[test]
|
||||
fn separations_preview_shows_a_dcs_image_through_its_own_plates() {
|
||||
use vectorcraft_doc::separated::{InkPlate, Separated};
|
||||
// Two pixels: the spot ink solid on the left, cyan solid on the right.
|
||||
let plates = vec![
|
||||
InkPlate { name: "Cyan".into(), cmyk: [0.0; 4], data: vec![0, 255] },
|
||||
InkPlate { name: "Ink".into(), cmyk: [0.0, 1.0, 1.0, 0.0], data: vec![255, 0] },
|
||||
];
|
||||
let s = Separated::new(2, 1, plates).unwrap();
|
||||
let mut d = Document::new_with_mode(40.0, 20.0, vectorcraft_doc::ColorMode::Cmyk);
|
||||
d.swatches.push(vectorcraft_color::Swatch { name: "Ink".into(), paint: Paint::solid(Color::cmyk(0.0, 1.0, 1.0, 0.0)), global: true, spot: true });
|
||||
d.images.insert("pic".into(), ImageBlob::separated_tiff(&s, None, &[]).unwrap());
|
||||
let im = ImageObject { key: "pic".into(), width: 2, height: 1, xf: Affine::scale(20.0), link: None, placement: Default::default(), pixels: None };
|
||||
let (layer, id) = (d.layers[0].id, d.alloc_id());
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let shown = |plates: &[&str]| {
|
||||
let out = Renderer::new().render(&d, 40, 20, Affine::IDENTITY, &separations(plates));
|
||||
(out.pixel(10, 10), out.pixel(30, 10))
|
||||
};
|
||||
let (left, right) = shown(&["Ink"]);
|
||||
assert!(left[..3].iter().all(|v| *v < 20), "the Ink plate shows the spot plate as solid ink, got {left:?}");
|
||||
assert!(right[..3].iter().all(|v| *v >= 250), "and nothing of cyan, got {right:?}");
|
||||
let (left, right) = shown(&["Cyan"]);
|
||||
assert!(left[..3].iter().all(|v| *v >= 250), "the Cyan plate shows nothing of the spot ink, got {left:?}");
|
||||
assert!(right[..3].iter().all(|v| *v < 20), "and the cyan plate as solid ink, got {right:?}");
|
||||
let (left, _) = shown(&["Cyan", "Magenta", "Yellow", "Black"]);
|
||||
assert!(left[..3].iter().all(|v| *v >= 250), "the process plates show paper where only the spot ink prints, got {left:?}");
|
||||
let (left, _) = shown(&["Cyan", "Magenta", "Yellow", "Black", "Ink"]);
|
||||
assert!(left[0] > 150 && left[1] < 100 && left[2] < 100, "with every plate the spot ink shows red, got {left:?}");
|
||||
let composite = Renderer::new().render(&d, 40, 20, Affine::IDENTITY, &RenderOptions { smooth_images: false, ..Default::default() }).pixel(10, 10);
|
||||
assert!(composite[0] > 150 && composite[1] < 100, "the composite mixes the spot ink in, got {composite:?}");
|
||||
// Without a spot swatch of the ink, the job prints no plate for it: it shows on the process
|
||||
// plates as its process equivalent, as it prints.
|
||||
d.swatches.retain(|s| s.name != "Ink");
|
||||
let process = Renderer::new().render(&d, 40, 20, Affine::IDENTITY, &separations(&["Cyan", "Magenta", "Yellow", "Black"])).pixel(10, 10);
|
||||
assert!(process[0] > 150 && process[1] < 100 && process[2] < 100, "the ink's magenta and yellow show, got {process:?}");
|
||||
let magenta = Renderer::new().render(&d, 40, 20, Affine::IDENTITY, &separations(&["Magenta"])).pixel(10, 10);
|
||||
assert!(magenta[..3].iter().all(|v| *v < 20), "the Magenta plate alone shows it as solid ink, got {magenta:?}");
|
||||
}
|
||||
|
||||
/// A document of 40 × 20 pt holding separated image `s` (2 × 1 pixels) at 20 pt a pixel.
|
||||
fn separated_doc(s: &vectorcraft_doc::separated::Separated) -> Document {
|
||||
let mut d = Document::new_with_mode(40.0, 20.0, vectorcraft_doc::ColorMode::Cmyk);
|
||||
d.images.insert("pic".into(), ImageBlob::separated_tiff(s, None, &[]).unwrap());
|
||||
let im = ImageObject {
|
||||
key: "pic".into(),
|
||||
width: s.width,
|
||||
height: s.height,
|
||||
xf: Affine::scale(40.0 / f64::from(s.width)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let (layer, id) = (d.layers[0].id, d.alloc_id());
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// A DCS spot plate shows in the color of the document's swatch of its ink, as the art on that ink
|
||||
/// does, and follows an edit of the swatch.
|
||||
#[test]
|
||||
fn separations_preview_colors_a_dcs_spot_plate_by_the_documents_swatch() {
|
||||
use vectorcraft_doc::separated::{InkPlate, Separated};
|
||||
// The file says the ink is red; the document's swatch of it is cyan.
|
||||
let s = Separated::new(2, 1, vec![InkPlate { name: "Ink".into(), cmyk: [0.0, 1.0, 1.0, 0.0], data: vec![255, 0] }]).unwrap();
|
||||
let mut d = separated_doc(&s);
|
||||
d.swatches.push(vectorcraft_color::Swatch { name: "Ink".into(), paint: Paint::solid(Color::cmyk(1.0, 0.0, 0.0, 0.0)), global: true, spot: true });
|
||||
let all = ["Cyan", "Magenta", "Yellow", "Black", "Ink"];
|
||||
let left = |d: &Document, plates: &[&str]| Renderer::new().render(d, 40, 20, Affine::IDENTITY, &separations(plates)).pixel(10, 10);
|
||||
let p = left(&d, &all);
|
||||
assert!(p[0] < 100 && p[2] > 150, "the swatch's cyan, not the file's red, got {p:?}");
|
||||
// The swatch edited to yellow: the image follows.
|
||||
if let Some(w) = d.swatches.iter_mut().find(|w| w.name == "Ink") {
|
||||
w.paint = Paint::solid(Color::cmyk(0.0, 0.0, 1.0, 0.0));
|
||||
}
|
||||
let p = left(&d, &all);
|
||||
assert!(p[0] > 150 && p[2] < 100, "the edited swatch's yellow, got {p:?}");
|
||||
// A process swatch of the ink's name: the plate shows on the process plates as that swatch.
|
||||
if let Some(w) = d.swatches.iter_mut().find(|w| w.name == "Ink") {
|
||||
(w.paint, w.spot) = (Paint::solid(Color::cmyk(1.0, 0.0, 0.0, 0.0)), false);
|
||||
}
|
||||
let p = left(&d, &all[..4]);
|
||||
assert!(p[0] < 100 && p[2] > 150, "the process swatch's cyan, got {p:?}");
|
||||
}
|
||||
|
||||
/// A DCS image drawn reduced (from a smaller mip level) shows the coverage its plate has there,
|
||||
/// the mean of the plate pixels each reduced pixel covers.
|
||||
#[test]
|
||||
fn separations_preview_keeps_a_reduced_dcs_plates_coverage() {
|
||||
use vectorcraft_doc::separated::{InkPlate, Separated};
|
||||
// 64 × 64 pixels, the spot ink solid on every other column: 50% coverage.
|
||||
let data: Vec<u8> = (0..64 * 64).map(|i| if i % 2 == 0 { 255 } else { 0 }).collect();
|
||||
let s = Separated::new(64, 64, vec![InkPlate { name: "Ink".into(), cmyk: [0.0, 1.0, 1.0, 0.0], data }]).unwrap();
|
||||
let mut d = Document::new_with_mode(64.0, 64.0, vectorcraft_doc::ColorMode::Cmyk);
|
||||
d.swatches.push(vectorcraft_color::Swatch { name: "Ink".into(), paint: Paint::solid(Color::cmyk(0.0, 1.0, 1.0, 0.0)), global: true, spot: true });
|
||||
d.images.insert("pic".into(), ImageBlob::separated_tiff(&s, None, &[]).unwrap());
|
||||
let im = ImageObject { key: "pic".into(), width: 64, height: 64, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
let (layer, id) = (d.layers[0].id, d.alloc_id());
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let opts = RenderOptions { image_mips: true, ..separations(&["Ink"]) };
|
||||
let p = Renderer::new().render(&d, 16, 16, Affine::scale(0.25), &opts).pixel(8, 8);
|
||||
assert!(p[..3].iter().all(|v| (100..=160).contains(v)), "half the ink, got {p:?}");
|
||||
}
|
||||
|
||||
/// Separations Preview showing `plates`, as the canvas draws it.
|
||||
fn separations(plates: &[&str]) -> RenderOptions {
|
||||
let proof = proof::ProofSetup { separations: Some(plates.iter().map(|p| p.to_string()).collect()), ..Default::default() };
|
||||
|
||||
@@ -771,6 +771,27 @@ fn rasterize_on_an_image_takes_in_its_strokes_shape() {
|
||||
assert_eq!((lum(img.pixel(8, 35)), lum(img.pixel(12, 35)), img.pixel(35, 35)), (WHITE, 0, [255, 0, 0, 255]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_images_outline_object_stroke_follows_new_pixels_under_the_same_key() {
|
||||
// The opaque image's stroke runs along its frame, from 18 to 22.
|
||||
let mut d = stroked_image_doc(vec![fx("path.outlineObject", json!({}))], false);
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let mut r = Renderer::new();
|
||||
let img = r.render(&d, 100, 100, Affine::IDENTITY, &opts);
|
||||
assert_eq!((lum(img.pixel(19, 35)), img.pixel(26, 35)), (0, [255, 0, 0, 255]));
|
||||
// New pixels under the same key, as Update Link stores them, with the node unchanged: an
|
||||
// opaque disc of radius 8 pixels in the middle. The stroke follows its edge at 26.5.
|
||||
let disc = image::RgbaImage::from_fn(30, 30, |x, y| {
|
||||
let r = (f64::from(x) + 0.5 - 15.0).hypot(f64::from(y) + 0.5 - 15.0);
|
||||
image::Rgba([255, 0, 0, if r <= 8.0 { 255 } else { 0 }])
|
||||
});
|
||||
let mut png = vec![];
|
||||
disc.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
d.images.insert("red".into(), ImageBlob::new("image/png", png));
|
||||
let img = r.render(&d, 100, 100, Affine::IDENTITY, &opts);
|
||||
assert_eq!((lum(img.pixel(19, 35)), lum(img.pixel(26, 35))), (WHITE, 0));
|
||||
}
|
||||
|
||||
/// A 40 pt square filled with `fill` carrying effect `id`, in a 100×100 document.
|
||||
fn glowing_square(fill: Color, id: &str, params: Value) -> Document {
|
||||
let mut n = Node::path(NodeId(0), shapes::rectangle(Rect::new(30.0, 30.0, 70.0, 70.0)), Appearance::basic(Paint::solid(fill), Paint::None, 0.0));
|
||||
|
||||
@@ -15,6 +15,9 @@ use super::{
|
||||
Ordering, Size,
|
||||
};
|
||||
|
||||
/// The most faces [`FontDb::postscript_faces`] gives, and the most families it loads.
|
||||
const MAX_POSTSCRIPT_FACES: usize = 16;
|
||||
|
||||
impl FontDb {
|
||||
/// The family's own name and the style for a name given in a document: any of a family's
|
||||
/// names (`ヒラギノ角ゴシック` is Hiragino Sans), a legacy family name (`Hiragino Sans W6` with its
|
||||
@@ -141,6 +144,33 @@ impl FontDb {
|
||||
loaded(self)
|
||||
}
|
||||
|
||||
/// Every installed face that [`find_postscript`](Self::find_postscript) could give for `name`
|
||||
/// (its PostScript name ignoring case and anything but letters and digits), its answer first,
|
||||
/// then the others in load order, at most 16 in all: copies of one font in
|
||||
/// other families (`SampleSerif` beside `Sample Serif`) or versions, and names that differ
|
||||
/// only in case. Each family that has one is loaded from the system fonts whole, as
|
||||
/// [`versions`](Self::versions) loads one. Empty when no installed font has the name.
|
||||
pub fn postscript_faces(&self, name: &str) -> Vec<Arc<FontFace>> {
|
||||
let Some(first) = self.find_postscript(name) else { return vec![] };
|
||||
let key = norm(name);
|
||||
let mut families: Vec<String> = vec![first.family.clone()];
|
||||
if let Some(v) = self.aliases.read().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
||||
for a in v.iter().filter(|a| a.paired.is_none() && a.style.is_some()) {
|
||||
if families.len() < MAX_POSTSCRIPT_FACES && !families.iter().any(|f| f.eq_ignore_ascii_case(&a.family)) {
|
||||
families.push(a.family.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for family in &families {
|
||||
self.load_family_files(family);
|
||||
}
|
||||
let mut out = vec![first.clone()];
|
||||
let faces = self.read_faces();
|
||||
let others = faces.iter().filter(|f| f.id != first.id && f.keys.postscript.contains(&key));
|
||||
out.extend(others.take(MAX_POSTSCRIPT_FACES - 1).cloned());
|
||||
out
|
||||
}
|
||||
|
||||
/// The (family, style) of the installed face whose PostScript name is `name` (ignoring ASCII
|
||||
/// case). Documents name fonts this way, and a family can hold hyphens
|
||||
/// ("Rounded-X-Mplus-1c-black" is Rounded-X M+ 1c, black), so the name can't be split.
|
||||
|
||||
@@ -1594,6 +1594,30 @@ pub fn twin_font(newer: bool) -> Option<Vec<u8>> {
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The names of a [`named_copy`]: family, style, version string and PostScript name.
|
||||
pub type CopyNames<'a> = (&'a str, &'a str, &'a str, &'a str);
|
||||
|
||||
/// The bundled Source Sans 3 Regular or, `inter`, the bundled Inter Regular (glyphs of other
|
||||
/// widths) under `names`, as an installed copy of a font: a font library can keep copies whose
|
||||
/// names differ only in case or punctuation, or whose family and style are one but whose PostScript
|
||||
/// names or versions differ. `None` when it can't be built.
|
||||
pub fn named_copy(inter: bool, (family, style, version, postscript): CopyNames<'_>) -> Option<Vec<u8>> {
|
||||
const SOURCE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
const INTER: &[u8] = include_bytes!("../../../assets/fonts/Inter-Regular.ttf");
|
||||
let font = skrifa::FontRef::new(if inter { INTER } else { SOURCE }).ok()?;
|
||||
let full = format!("{family} {style}");
|
||||
let name = name_table(&[(1, family), (2, style), (4, &full), (5, version), (6, postscript)])?;
|
||||
let mut builder = FontBuilder::new();
|
||||
builder.add_raw(Tag::new(b"name"), name);
|
||||
for r in font.table_directory.table_records() {
|
||||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||||
if !builder.contains(tag) {
|
||||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||||
}
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The family of [`numbered_font`]: styles numbered W3 and W6, and no Regular.
|
||||
pub const NUMBERED_FAMILY: &str = "Numtest Mincho";
|
||||
|
||||
|
||||
@@ -41,3 +41,26 @@ fn type_naming_a_version_is_set_in_it() {
|
||||
assert!((newer - older).abs() > 50.0, "the two versions set the line differently: {newer} vs {older}");
|
||||
assert_eq!(width(&db, Some(VERSIONS[1])), newer);
|
||||
}
|
||||
|
||||
/// Every installed face a PostScript name answers to, ignoring case and punctuation: the face
|
||||
/// `find_postscript` gives first, then the copies in other families and the versions, in load
|
||||
/// order; none for a name no face has.
|
||||
#[test]
|
||||
fn every_face_of_a_postscript_name_is_found() {
|
||||
use crate::test_fonts::named_copy;
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
for (inter, names) in [
|
||||
(true, ("Facetest Ink", "Bold", "Version 1.000", "FacetestInkBold")),
|
||||
(false, ("Facetest-Ink", "Bold", "Version 1.000", "FacetestInk-Bold")),
|
||||
(true, ("Facetest-Ink", "Bold", "Version 2.000", "FACETESTINK-BOLD")),
|
||||
(false, ("Other Ink", "Bold", "Version 1.000", "OtherInk-Bold")),
|
||||
] {
|
||||
assert_eq!(db.add_font(named_copy(inter, names).expect("the test font builds")), 1, "{names:?}");
|
||||
}
|
||||
let names = |name: &str| db.postscript_faces(name).iter().map(|f| f.postscript_name()).collect::<Vec<_>>();
|
||||
let first = db.find_postscript("FacetestInk-Bold").expect("a face").postscript_name();
|
||||
assert_eq!(first, "FacetestInkBold");
|
||||
assert_eq!(names("FacetestInk-Bold"), ["FacetestInkBold", "FacetestInk-Bold", "FACETESTINK-BOLD"]);
|
||||
assert_eq!(names("OtherInk-Bold"), ["OtherInk-Bold"]);
|
||||
assert!(names("NoSuchInk-Bold").is_empty());
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@ impl SmartPenTool {
|
||||
}
|
||||
Target::Close { id, si, first, mut anchor } => {
|
||||
let out = Self::handle_to(cx, first, ev.pos, first, shift, false);
|
||||
let act = close_drag(id, Some(si), first - (out - first), alt, anchor.as_mut());
|
||||
let act = close_drag(id, Some(si), None, first - (out - first), alt, anchor.as_mut());
|
||||
pr.what = Target::Close { id, si, first, anchor };
|
||||
("", act)
|
||||
}
|
||||
|
||||
+35
-15
@@ -7,7 +7,8 @@
|
||||
//! incoming handle mirrors the pointer (a click leaves it retracted and the anchor a corner, so the
|
||||
//! closing segment comes in straight). Its outgoing handle stays where it is on a corner, so the first segment keeps its
|
||||
//! shape, and on a smooth anchor turns to stay in line at its own length; Alt makes the anchor a
|
||||
//! corner, leaving the outgoing handle where it was when Alt went down. Clicking the
|
||||
//! corner, leaving the outgoing handle where it was when Alt went down; Space held moves the
|
||||
//! anchor, handles and all. Clicking the
|
||||
//! last one retracts its outgoing handle, so the next segment leaves it as a corner; dragging from
|
||||
//! it pulls a new one out on its own.
|
||||
//! Alt held over a handle end, an anchor or a segment of a selected path (the one being drawn too)
|
||||
@@ -76,13 +77,15 @@ const DRAG_THRESHOLD: f64 = 3.0;
|
||||
/// An anchor being placed, as the press decided: where it is, the path it goes on (none: the
|
||||
/// first anchor of a new path) and whether it closes that path (the press was on its first
|
||||
/// anchor). The drag only shapes its handles, whatever the path looks like meanwhile: a closing
|
||||
/// preview leaves the path closed, so no longer one the pen draws on.
|
||||
/// preview leaves the path closed, so no longer one the pen draws on. `moved`: Space held has
|
||||
/// moved the anchor from where the press found it.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Place {
|
||||
at: Point,
|
||||
path: Option<NodeId>,
|
||||
subpath: Option<usize>,
|
||||
closing: bool,
|
||||
moved: bool,
|
||||
}
|
||||
|
||||
/// The open path the pen is extending: the single selected open path, or the single open path of a
|
||||
@@ -329,7 +332,7 @@ impl Tool for PenTool {
|
||||
params["subpaths"] = json!([si, other_si]);
|
||||
return vec![Action::Exec("path.join".into(), params)];
|
||||
}
|
||||
self.drag = Some(Place { at: p, path: Some(id), subpath: Some(si), closing: false });
|
||||
self.drag = Some(Place { at: p, path: Some(id), subpath: Some(si), closing: false, moved: false });
|
||||
self.in_h = p;
|
||||
let params = json!({"id": id.0, "x": p.x, "y": p.y, "subpath": si});
|
||||
return vec![Action::Begin("Pen".into()), Action::Preview("path.appendAnchor".into(), params)];
|
||||
@@ -366,7 +369,7 @@ impl Tool for PenTool {
|
||||
return vec![act];
|
||||
}
|
||||
(self.drawing, self.path, self.subpath) = (true, None, None);
|
||||
self.drag = Some(Place { at: p, path: None, subpath: None, closing: false });
|
||||
self.drag = Some(Place { at: p, path: None, subpath: None, closing: false, moved: false });
|
||||
vec![Action::Begin("Pen".into()), Action::Preview("path.create".into(), json!({"anchors": [{"x": p.x, "y": p.y}]}))]
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
@@ -390,13 +393,19 @@ impl Tool for PenTool {
|
||||
}
|
||||
return vec![set_out_handle(id, si, ai, h)];
|
||||
}
|
||||
let Some(Place { at: mut a, path, subpath, closing }) = self.drag else { return vec![] };
|
||||
// Space held moves the anchor being placed, handles and all.
|
||||
if ev.mods.space && !closing {
|
||||
let Some(Place { at: mut a, path, subpath, closing, mut moved }) = self.drag else { return vec![] };
|
||||
// Space held moves the anchor being placed, handles and all; while closing, the
|
||||
// first anchor, with the outgoing handle a closing drag keeps.
|
||||
if ev.mods.space {
|
||||
let d = ev.pos - self.last;
|
||||
a += d;
|
||||
self.in_h += d;
|
||||
self.drag = Some(Place { at: a, path, subpath, closing });
|
||||
if closing && let Some((anchor, out)) = &mut self.first {
|
||||
anchor.translate(d);
|
||||
*out += d;
|
||||
}
|
||||
moved |= d != Vec2::ZERO;
|
||||
self.drag = Some(Place { at: a, path, subpath, closing, moved });
|
||||
}
|
||||
self.last = ev.pos;
|
||||
let mut out_h = ev.pos;
|
||||
@@ -413,7 +422,7 @@ impl Tool for PenTool {
|
||||
)];
|
||||
};
|
||||
if closing {
|
||||
return vec![close_drag(id, subpath, a - (out_h - a), alt, self.first.as_mut())];
|
||||
return vec![close_drag(id, subpath, moved.then_some(a), a - (out_h - a), alt, self.first.as_mut())];
|
||||
}
|
||||
// Alt pressed mid-drag keeps the curve already shaped into the anchor: only the
|
||||
// outgoing handle follows. Held from the start, the incoming handle stays retracted.
|
||||
@@ -583,7 +592,7 @@ impl PenTool {
|
||||
/// anchor a corner with its incoming handle retracted until a drag pulls it out, as a click on
|
||||
/// the last anchor retracts its outgoing one ([`set_out_handle`]).
|
||||
fn begin_close(&mut self, cx: &ToolContext, id: NodeId, si: usize, at: Point) -> Vec<Action> {
|
||||
self.drag = Some(Place { at, path: Some(id), subpath: Some(si), closing: true });
|
||||
self.drag = Some(Place { at, path: Some(id), subpath: Some(si), closing: true, moved: false });
|
||||
self.first = first_anchor(cx, id, si).map(|a| (a, a.h_out));
|
||||
let params = json!({"id": id.0, "subpath": si, "in": [at.x, at.y], "independent": true});
|
||||
vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), params)]
|
||||
@@ -609,12 +618,23 @@ pub(crate) fn first_anchor(cx: &ToolContext, id: NodeId, si: usize) -> Option<An
|
||||
cx.doc.node(id)?.path_data()?.subpaths.get(si)?.anchors.first().copied()
|
||||
}
|
||||
|
||||
/// The preview of a closing drag that puts the first anchor's incoming handle at `in_h`. `first`
|
||||
/// holds that anchor as it was when the press began closing the path, and its outgoing handle as
|
||||
/// the drag last left it: a smooth anchor turns that handle to stay in line, and Alt (`alt`), which
|
||||
/// makes the anchor a corner, keeps it there.
|
||||
pub(crate) fn close_drag(id: NodeId, subpath: Option<usize>, in_h: Point, alt: bool, first: Option<&mut (Anchor, Point)>) -> Action {
|
||||
/// The preview of a closing drag that puts the first anchor's incoming handle at `in_h`, the anchor
|
||||
/// first moved to `at` when given (Space held). `first` holds that anchor as it was when the press
|
||||
/// began closing the path (moved with it), and its outgoing handle as the drag last left it: a
|
||||
/// smooth anchor turns that handle to stay in line, and Alt (`alt`), which makes the anchor a
|
||||
/// corner, keeps it there.
|
||||
pub(crate) fn close_drag(
|
||||
id: NodeId,
|
||||
subpath: Option<usize>,
|
||||
at: Option<Point>,
|
||||
in_h: Point,
|
||||
alt: bool,
|
||||
first: Option<&mut (Anchor, Point)>,
|
||||
) -> Action {
|
||||
let mut params = json!({"id": id.0, "in": [in_h.x, in_h.y], "independent": alt});
|
||||
if let Some(at) = at {
|
||||
params["at"] = json!([at.x, at.y]);
|
||||
}
|
||||
if let Some((anchor, out)) = first {
|
||||
if alt {
|
||||
params["out"] = json!([out.x, out.y]);
|
||||
|
||||
@@ -628,6 +628,43 @@ fn alt_mid_close_keeps_the_turned_outgoing_handle() {
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 130.0, 200.0)), vec![Action::Commit]);
|
||||
}
|
||||
|
||||
/// Space held during a closing drag moves the first anchor, handles and all, by as far as the
|
||||
/// pointer moves; released, the drag shapes the handles round the anchor's new place.
|
||||
#[test]
|
||||
fn space_moves_the_closing_anchor() {
|
||||
use vectorcraft_geom::{Anchor, SubPath};
|
||||
let anchors = vec![Anchor::smooth(Point::new(100.0, 300.0), Point::new(150.0, 300.0)), Anchor::corner(Point::new(200.0, 200.0))];
|
||||
let (mut d, id, s) = drawing(SubPath { anchors, closed: false });
|
||||
let p = paint();
|
||||
let mut t = PenTool { drawing: true, ..PenTool::default() };
|
||||
assert_eq!(t.pointer(&cx(&d, &s, &p), &PointerEvent::new(PointerKind::Down, 100.0, 300.0)), close_press(id, Point::new(100.0, 300.0)));
|
||||
d.node_mut(id).unwrap().path_data_mut().unwrap().subpaths[0].closed = true;
|
||||
let c = cx(&d, &s, &p);
|
||||
let close = |v: serde_json::Value| vec![Action::Preview("path.close".into(), v)];
|
||||
assert_eq!(
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 100.0, 200.0)),
|
||||
close(json!({"id": id.0, "in": [100.0, 400.0], "independent": false, "subpath": 0}))
|
||||
);
|
||||
let space = Mods { space: true, ..Mods::default() };
|
||||
assert_eq!(
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 130.0, 180.0).with_mods(space)),
|
||||
close(json!({"id": id.0, "at": [130.0, 280.0], "in": [130.0, 380.0], "independent": false, "subpath": 0})),
|
||||
"moved, handles and all"
|
||||
);
|
||||
// Space released: the drag shapes the handles round the anchor's new place.
|
||||
assert_eq!(
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 130.0, 230.0)),
|
||||
close(json!({"id": id.0, "at": [130.0, 280.0], "in": [130.0, 330.0], "independent": false, "subpath": 0}))
|
||||
);
|
||||
// Alt keeps the outgoing handle where the drag had turned it, moved with the anchor.
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
assert_eq!(
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 160.0, 230.0).with_mods(alt)),
|
||||
close(json!({"id": id.0, "at": [130.0, 280.0], "in": [100.0, 330.0], "out": [130.0, 230.0], "independent": true, "subpath": 0}))
|
||||
);
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 160.0, 230.0)), vec![Action::Commit]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_one_anchor_path_does_not_close_on_itself() {
|
||||
let (d, id, s) = drawing(vectorcraft_geom::SubPath::polyline(&[Point::new(10.0, 300.0)], false));
|
||||
|
||||
@@ -239,7 +239,8 @@ pub(crate) struct MoveSnap {
|
||||
copy_targets: Option<Targets>,
|
||||
copy_points: Option<Targets>,
|
||||
/// With Smart Guides on, the ruler guides View › Snap to Point still pulls the grabbed point
|
||||
/// onto (#1534), and lines the bounds up with (#5746); `points` holds them with Smart Guides off.
|
||||
/// onto (#1534), and lines the bounds up with (#5746), within the Snapping Tolerance as drawing
|
||||
/// does; `points` holds them with Smart Guides off.
|
||||
rulers: Option<Targets>,
|
||||
}
|
||||
|
||||
@@ -303,7 +304,7 @@ impl MoveSnap {
|
||||
// axis without one that Smart Guides left free, the bounds line up with the ruler guides
|
||||
// (#5746), alongside the Smart Guides.
|
||||
if let Some(t) = &self.rulers {
|
||||
let (adj, ov, held) = t.snap_moved_to_rulers(start + d, self.bounds.map(|b| b + d), cx.tol(cx.snap_tolerance), taken);
|
||||
let (adj, ov, held) = t.snap_moved_to_rulers(start + d, self.bounds.map(|b| b + d), cx.snap_tol(), taken);
|
||||
if adj != Vec2::ZERO {
|
||||
d += adj;
|
||||
if held {
|
||||
@@ -373,7 +374,7 @@ impl MoveSnap {
|
||||
}
|
||||
}
|
||||
if let Some(t) = &self.rulers {
|
||||
let (q, ov) = t.snap_point_along(start + d, u, cx.tol(cx.snap_tolerance));
|
||||
let (q, ov) = t.snap_point_along(start + d, u, cx.snap_tol());
|
||||
if q != start + d {
|
||||
d = q - start;
|
||||
guides = ov;
|
||||
@@ -382,7 +383,7 @@ impl MoveSnap {
|
||||
{
|
||||
// Nothing else lined up: the bounds slide along the line onto a ruler guide, as
|
||||
// they do without Shift (#4312).
|
||||
let (adj, ov) = t.snap_moved_along_to_rulers(start + d, b + d, u, cx.tol(cx.snap_tolerance));
|
||||
let (adj, ov) = t.snap_moved_along_to_rulers(start + d, b + d, u, cx.snap_tol());
|
||||
d += adj;
|
||||
guides = ov;
|
||||
}
|
||||
|
||||
@@ -751,7 +751,7 @@ fn escape_cancels_a_move_or_an_alt_drag_copy() {
|
||||
}
|
||||
|
||||
/// #1534: with Smart Guides on (the default), the point a selection is dragged by still snaps
|
||||
/// to a ruler guide within the Snap to Point distance; not with Snap to Point off.
|
||||
/// to a ruler guide within the Snapping Tolerance; not with Snap to Point off.
|
||||
#[test]
|
||||
fn a_move_snaps_the_grabbed_point_to_a_ruler_guide_with_smart_guides_on() {
|
||||
let (mut d, id) = doc_with_rect();
|
||||
@@ -797,6 +797,39 @@ fn a_shift_move_lands_the_edges_on_a_ruler_guide() {
|
||||
assert!(moved_by(m, (231.0, 0.0)), "{m:?}");
|
||||
}
|
||||
|
||||
/// With Smart Guides on, a moved selection reaches a ruler guide within the Snapping Tolerance
|
||||
/// (4 px), as drawing and handle drags do; with Smart Guides off, Snap to Point reaches it within
|
||||
/// its own distance (2 px).
|
||||
#[test]
|
||||
fn a_move_reaches_ruler_guides_within_the_snapping_tolerance() {
|
||||
let (mut d, id) = doc_with_rect();
|
||||
d.add_ruler_guide(&vectorcraft_doc::Guide::new(true, 340.0));
|
||||
let mut s = Selection::default();
|
||||
s.add(id);
|
||||
let p = paint();
|
||||
let dx = |c: &ToolContext, to: f64| {
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(c, &ev(PointerKind::Down, 150.0, 150.0));
|
||||
let a = t.pointer(c, &ev(PointerKind::Drag, to, 150.0));
|
||||
let Some(Action::Preview(_, v)) = a.last() else { panic!("{a:?}") };
|
||||
v["matrix"][4].as_f64().unwrap()
|
||||
};
|
||||
let c = cx(&d, &s, &p);
|
||||
assert!(c.smart_guides && c.snap_to_point && c.guides);
|
||||
// The right edge, 3 px short of the guide, lands on it, with or without Shift.
|
||||
assert_eq!(dx(&c, 150.0 + 137.0), 140.0);
|
||||
let (m, _) = shift_moved(&c, (150.0 + 137.0, 150.0));
|
||||
assert!(moved_by(m, (140.0, 0.0)), "{m:?}");
|
||||
// The grabbed point, 3 px from the guide, lands on it.
|
||||
assert_eq!(dx(&c, 337.0), 190.0);
|
||||
// 5 px away is out of reach.
|
||||
assert_eq!(dx(&c, 150.0 + 135.0), 135.0);
|
||||
// Smart Guides off: the grabbed point lands on the guide from 1.5 px, not from 3 px.
|
||||
let off = ToolContext { smart_guides: false, ..cx(&d, &s, &p) };
|
||||
assert_eq!(dx(&off, 338.5), 190.0);
|
||||
assert_eq!(dx(&off, 337.0), 187.0);
|
||||
}
|
||||
|
||||
/// Alt pressed during a move makes a copy, which snaps to the original it leaves behind (as
|
||||
/// gathered before the previews moved it); released, the move doesn't.
|
||||
#[test]
|
||||
|
||||
@@ -51,6 +51,7 @@ mod gradient;
|
||||
mod group;
|
||||
mod logo;
|
||||
mod mosaic;
|
||||
mod opaque;
|
||||
mod quantize;
|
||||
mod shapes;
|
||||
mod smooth;
|
||||
@@ -64,6 +65,7 @@ use vectorcraft_geom::PathData;
|
||||
pub use contour::{Component, Loop, trace_mask};
|
||||
pub use gradient::TracedGradient;
|
||||
pub use mosaic::mosaic;
|
||||
pub use opaque::{ALPHA_MAX_DECODE_BYTES, OUTLINE_MAX_ANCHORS, OUTLINE_MAX_PIXELS, decode_alpha, opaque_outline};
|
||||
pub use quantize::{FULL_TONE_MAX, Quantized, TRANSPARENT, denoise, quantize};
|
||||
pub use shapes::TracedShape;
|
||||
use subpixel::Crack;
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
//! The outline of an image's opaque area: what Outline Object on an image's own fill or stroke
|
||||
//! follows.
|
||||
|
||||
use vectorcraft_geom::{Affine, PathData};
|
||||
|
||||
use crate::raster::{MAX_PIXELS, fit_size, gif_first_frame_pixels};
|
||||
use crate::{Mode, Raster, TraceError, TraceParams, trace_within};
|
||||
|
||||
/// The most pixels [`opaque_outline`] traces: a larger image is reduced to this many first,
|
||||
/// aspect kept ([`reduced`]), and its outline is scaled back up to the image's size.
|
||||
pub const OUTLINE_MAX_PIXELS: u64 = 4 << 20;
|
||||
|
||||
/// The most anchor points an outline may have; an image whose outline would have more gives
|
||||
/// [`TraceError::TooComplex`].
|
||||
pub const OUTLINE_MAX_ANCHORS: usize = 100_000;
|
||||
|
||||
/// The most bytes [`decode_alpha`] decodes an image's samples into (256 MB: 64 megapixels at 8
|
||||
/// bits a channel, 32 megapixels at 16).
|
||||
pub const ALPHA_MAX_DECODE_BYTES: u64 = 256 << 20;
|
||||
|
||||
/// The alpha of the encoded image `bytes` (PNG, WebP, GIF, TIFF and the other formats the image
|
||||
/// reader takes), one byte a pixel, row by row, with the image's width and height. `None` when the
|
||||
/// image's format or color type has no alpha; the image is then not decoded. An image of more
|
||||
/// than [`MAX_PIXELS`] (64 megapixels), or whose samples take more than [`ALPHA_MAX_DECODE_BYTES`],
|
||||
/// gives [`TraceError::TooLarge`] before it is decoded.
|
||||
pub fn decode_alpha(bytes: &[u8]) -> Result<Option<(u32, u32, Vec<u8>)>, TraceError> {
|
||||
use image::ImageDecoder;
|
||||
let decode_err = |e: image::ImageError| TraceError::Decode(e.to_string());
|
||||
let reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().map_err(|e| TraceError::Decode(e.to_string()))?;
|
||||
let decoder = reader.into_decoder().map_err(decode_err)?;
|
||||
let (w, h) = decoder.dimensions();
|
||||
if w == 0 || h == 0 {
|
||||
return Err(TraceError::Empty);
|
||||
}
|
||||
if !decoder.color_type().has_alpha() {
|
||||
return Ok(None);
|
||||
}
|
||||
let pixels = u64::from(w) * u64::from(h);
|
||||
let frame = gif_first_frame_pixels(bytes).unwrap_or(0);
|
||||
if pixels.max(frame) > MAX_PIXELS || decoder.total_bytes() > ALPHA_MAX_DECODE_BYTES {
|
||||
return Err(TraceError::TooLarge { width: w, height: h, max: MAX_PIXELS >> 20 });
|
||||
}
|
||||
let alpha: Vec<u8> = match image::DynamicImage::from_decoder(decoder).map_err(decode_err)? {
|
||||
image::DynamicImage::ImageRgba8(img) => img.as_raw().as_chunks::<4>().0.iter().map(|p| p[3]).collect(),
|
||||
image::DynamicImage::ImageLumaA8(img) => img.as_raw().as_chunks::<2>().0.iter().map(|p| p[1]).collect(),
|
||||
img => img.to_rgba8().as_raw().as_chunks::<4>().0.iter().map(|p| p[3]).collect(),
|
||||
};
|
||||
Ok(Some((w, h, alpha)))
|
||||
}
|
||||
|
||||
/// The outline of the pixels of a `width × height` image whose `alpha` (one byte a pixel, row by
|
||||
/// row) is at least half opaque (128 or more), in its pixel space (x right, y down; the image
|
||||
/// spans `0..width × 0..height`). `None` when every pixel is: the outline is then the image's
|
||||
/// frame. An image with no such pixel gives an empty outline.
|
||||
///
|
||||
/// The alpha is traced as Image Trace's Black and White mode traces a gray at threshold 128, with
|
||||
/// its Paths and Corners at their defaults and no area left out as noise: the edge runs where the
|
||||
/// alpha crosses one half, to a fraction of a pixel along anti-aliased edges, and the outline is
|
||||
/// fitted with curves. Outer outlines and holes run in opposite directions, and an island in a
|
||||
/// hole runs as outer outlines do, so the outline fills the same with either fill rule. An image
|
||||
/// of more than [`OUTLINE_MAX_PIXELS`] is traced at that size ([`reduced`]).
|
||||
pub fn opaque_outline(width: u32, height: u32, alpha: Vec<u8>) -> Result<Option<PathData>, TraceError> {
|
||||
let (w, h) = (width, height);
|
||||
if (w as usize).checked_mul(h as usize).is_none_or(|n| n == 0 || n != alpha.len()) {
|
||||
return Err(TraceError::Empty);
|
||||
}
|
||||
if alpha.iter().all(|&a| a >= 128) {
|
||||
return Ok(None);
|
||||
}
|
||||
if alpha.iter().all(|&a| a < 128) {
|
||||
return Ok(Some(PathData::default()));
|
||||
}
|
||||
let (nw, nh) = fit_size(w, h, OUTLINE_MAX_PIXELS);
|
||||
let alpha = if (nw, nh) == (w, h) { alpha } else { reduced(&alpha, w, h, nw, nh) };
|
||||
// Opaque pixels are black, transparent ones white.
|
||||
let mut rgba = Vec::with_capacity(alpha.len() * 4);
|
||||
for a in alpha {
|
||||
let g = 255 - a;
|
||||
rgba.extend_from_slice(&[g, g, g, 255]);
|
||||
}
|
||||
let gray = Raster::new(nw, nh, rgba)?;
|
||||
let params = TraceParams { mode: Mode::BlackAndWhite, threshold: 128, ignore_white: true, noise: 0, ..TraceParams::default() };
|
||||
let traced = trace_within(&gray, ¶ms, OUTLINE_MAX_ANCHORS)?;
|
||||
let mut out = PathData::default();
|
||||
for p in traced.paths {
|
||||
out.subpaths.extend(p.path.subpaths);
|
||||
}
|
||||
if (nw, nh) != (w, h) {
|
||||
out = out.transformed(Affine::scale_non_uniform(f64::from(w) / f64::from(nw), f64::from(h) / f64::from(nh)));
|
||||
}
|
||||
Ok(Some(out))
|
||||
}
|
||||
|
||||
/// The `w × h` `alpha` reduced to `nw × nh` (at most `w × h`): each pixel holds the mean of the
|
||||
/// block of pixels it covers, and at least 128 when that block holds a pixel of 128 or more, so an
|
||||
/// opaque line thinner than a block stays inside the outline. Along an edge the outline can then
|
||||
/// run up to half a block further out than the full-size image's would.
|
||||
fn reduced(alpha: &[u8], w: u32, h: u32, nw: u32, nh: u32) -> Vec<u8> {
|
||||
let span = |i: u32, n: u32, of: u32| {
|
||||
let at = |i: u32| usize::try_from(u64::from(i) * u64::from(of) / u64::from(n.max(1))).unwrap_or(usize::MAX);
|
||||
let start = at(i);
|
||||
start..at(i + 1).max(start + 1)
|
||||
};
|
||||
let cols: Vec<_> = (0..nw).map(|x| span(x, nw, w)).collect();
|
||||
let mut out = Vec::with_capacity(nw as usize * nh as usize);
|
||||
let (mut sums, mut maxes) = (vec![0u64; cols.len()], vec![0u8; cols.len()]);
|
||||
for ty in 0..nh {
|
||||
let rows = span(ty, nh, h);
|
||||
sums.fill(0);
|
||||
maxes.fill(0);
|
||||
for y in rows.clone() {
|
||||
let Some(row) = alpha.get(y * w as usize..(y + 1) * w as usize) else { break };
|
||||
for (x, c) in cols.iter().enumerate() {
|
||||
for &a in row.get(c.clone()).unwrap_or_default() {
|
||||
sums[x] += u64::from(a);
|
||||
maxes[x] = maxes[x].max(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (x, c) in cols.iter().enumerate() {
|
||||
let count = (rows.len() * c.len()).max(1) as u64;
|
||||
let mean = u8::try_from((sums[x] + count / 2) / count).unwrap_or(u8::MAX);
|
||||
out.push(if maxes[x] >= 128 { mean.max(128) } else { mean });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kurbo::Shape;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// The width, height and alpha of `img`, as [`opaque_outline`] takes them.
|
||||
fn alpha(img: &Raster) -> (u32, u32, Vec<u8>) {
|
||||
(img.width, img.height, img.rgba.as_chunks::<4>().0.iter().map(|p| p[3]).collect())
|
||||
}
|
||||
|
||||
fn outline(img: &Raster) -> Result<Option<PathData>, TraceError> {
|
||||
let (w, h, a) = alpha(img);
|
||||
opaque_outline(w, h, a)
|
||||
}
|
||||
|
||||
/// A `w × h` raster, opaque red inside the disc of radius `r` around the middle and
|
||||
/// transparent outside it.
|
||||
fn disc(w: u32, h: u32, r: f64) -> Raster {
|
||||
let (cx, cy) = (f64::from(w) / 2.0, f64::from(h) / 2.0);
|
||||
Raster::from_fn(w, h, |x, y| {
|
||||
let (dx, dy) = (f64::from(x) + 0.5 - cx, f64::from(y) + 0.5 - cy);
|
||||
if dx.hypot(dy) <= r { [255, 0, 0, 255] } else { [0, 0, 0, 0] }
|
||||
})
|
||||
}
|
||||
|
||||
fn area(p: &PathData) -> f64 {
|
||||
p.to_bezpath().area().abs()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_outline_follows_the_opaque_disc_not_the_frame() {
|
||||
let outline = outline(&disc(100, 100, 30.0)).unwrap().unwrap();
|
||||
let b = outline.bounds().unwrap();
|
||||
for (got, want) in [(b.x0, 20.0), (b.y0, 20.0), (b.x1, 80.0), (b.y1, 80.0)] {
|
||||
assert!((got - want).abs() < 1.0, "{b:?}");
|
||||
}
|
||||
let disc_area = std::f64::consts::PI * 30.0 * 30.0;
|
||||
assert!((area(&outline) - disc_area).abs() < disc_area * 0.02, "{}", area(&outline));
|
||||
// Fitted with curves, not one line per pixel step.
|
||||
assert!(outline.anchor_count() < 40, "{} anchors", outline.anchor_count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_opaque_image_keeps_its_frame_and_a_transparent_one_has_no_outline() {
|
||||
let opaque = Raster::from_fn(10, 10, |_, _| [0, 0, 255, 128]);
|
||||
assert_eq!(outline(&opaque).unwrap(), None);
|
||||
let clear = Raster::from_fn(10, 10, |_, _| [0, 0, 255, 127]);
|
||||
assert!(outline(&clear).unwrap().unwrap().is_empty());
|
||||
// One pixel either way.
|
||||
assert_eq!(outline(&Raster::from_fn(1, 1, |_, _| [9, 9, 9, 255])).unwrap(), None);
|
||||
assert!(outline(&Raster::from_fn(1, 1, |_, _| [9, 9, 9, 0])).unwrap().unwrap().is_empty());
|
||||
// No pixels at all, or an alpha that doesn't match the size.
|
||||
assert!(opaque_outline(0, 0, vec![]).is_err());
|
||||
assert!(opaque_outline(2, 2, vec![0]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixels_less_than_half_opaque_are_outside() {
|
||||
// A 20 px square, opaque, inside a 6 px band of alpha `band`, on a transparent 40 px image.
|
||||
let framed = |band: u8| {
|
||||
Raster::from_fn(40, 40, |x, y| {
|
||||
let d = x.min(y).min(39 - x).min(39 - y);
|
||||
let alpha = match d {
|
||||
10.. => 255,
|
||||
4.. => band,
|
||||
_ => 0,
|
||||
};
|
||||
[0, 0, 0, alpha]
|
||||
})
|
||||
};
|
||||
let inner = outline(&framed(127)).unwrap().unwrap().bounds().unwrap();
|
||||
assert!((inner.x0 - 10.0).abs() < 0.5 && (inner.x1 - 30.0).abs() < 0.5, "{inner:?}");
|
||||
// A half-opaque band is inside; the edge runs through the middle of its outer pixels,
|
||||
// which are half covered.
|
||||
let outer = outline(&framed(128)).unwrap().unwrap().bounds().unwrap();
|
||||
assert!((outer.x0 - 4.5).abs() < 0.25 && (outer.x1 - 35.5).abs() < 0.25, "{outer:?}");
|
||||
let outer = outline(&framed(255)).unwrap().unwrap().bounds().unwrap();
|
||||
assert!((outer.x0 - 4.0).abs() < 0.25 && (outer.x1 - 36.0).abs() < 0.25, "{outer:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hole_in_the_opaque_area_is_a_hole_in_the_outline() {
|
||||
let ring = Raster::from_fn(60, 60, |x, y| {
|
||||
let d = (f64::from(x) + 0.5 - 30.0).hypot(f64::from(y) + 0.5 - 30.0);
|
||||
[0, 0, 0, if (10.0..=25.0).contains(&d) { 255 } else { 0 }]
|
||||
});
|
||||
let outline = outline(&ring).unwrap().unwrap();
|
||||
let want = std::f64::consts::PI * (25.0 * 25.0 - 10.0 * 10.0);
|
||||
assert!((outline.to_bezpath().area().abs() - want).abs() < want * 0.03, "{}", outline.to_bezpath().area());
|
||||
assert!(!outline.to_bezpath().contains(kurbo::Point::new(30.0, 30.0)));
|
||||
assert!(outline.to_bezpath().contains(kurbo::Point::new(30.0, 12.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_outline_fills_the_same_with_either_fill_rule() {
|
||||
// A ring with an opaque island in its hole.
|
||||
let target = Raster::from_fn(80, 80, |x, y| {
|
||||
let d = (f64::from(x) + 0.5 - 40.0).hypot(f64::from(y) + 0.5 - 40.0);
|
||||
[0, 0, 0, if (25.0..=35.0).contains(&d) || d <= 10.0 { 255 } else { 0 }]
|
||||
});
|
||||
let path = outline(&target).unwrap().unwrap().to_bezpath();
|
||||
let winding = |x: f64, y: f64| path.winding(kurbo::Point::new(x, y));
|
||||
let (ring, hole, island, outside) = (winding(40.0, 10.0), winding(40.0, 20.0), winding(40.0, 40.0), winding(2.0, 2.0));
|
||||
// Non-zero and even-odd agree when every winding number is -1, 0 or 1.
|
||||
assert!(ring.abs() == 1 && island == ring && hole == 0 && outside == 0, "{ring} {hole} {island} {outside}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_large_image_is_traced_smaller_and_scaled_back() {
|
||||
let big = disc(2400, 2000, 800.0);
|
||||
assert!(u64::from(big.width) * u64::from(big.height) > OUTLINE_MAX_PIXELS);
|
||||
let b = outline(&big).unwrap().unwrap().bounds().unwrap();
|
||||
for (got, want) in [(b.x0, 400.0), (b.y0, 200.0), (b.x1, 2000.0), (b.y1, 1800.0)] {
|
||||
assert!((got - want).abs() < 3.0, "{b:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_line_thinner_than_the_reduction_stays_in_a_large_images_outline() {
|
||||
// 39 megapixels, reduced to 4: each traced pixel covers about 3 × 3. The only opaque
|
||||
// pixels are a line 1 pixel wide, which averages to about a third opaque.
|
||||
let (w, h) = (8192u32, 4800u32);
|
||||
let mut a = vec![0u8; (w * h) as usize];
|
||||
for y in 1000..3000 {
|
||||
a[(y * w + 2001) as usize] = 255;
|
||||
}
|
||||
let b = opaque_outline(w, h, a).unwrap().unwrap().bounds().unwrap();
|
||||
assert!(b.x0 > 1990.0 && b.x1 < 2012.0 && (b.y0 - 1000.0).abs() < 6.0 && (b.y1 - 3000.0).abs() < 6.0, "{b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_outline_of_too_many_anchors_is_refused() {
|
||||
// A checkerboard of 2 px squares: more than 50,000 opaque squares.
|
||||
let checker = Raster::from_fn(640, 640, |x, y| [0, 0, 0, if (x / 2 + y / 2) % 2 == 0 { 255 } else { 0 }]);
|
||||
assert!(matches!(outline(&checker), Err(TraceError::TooComplex { max: OUTLINE_MAX_ANCHORS })));
|
||||
}
|
||||
|
||||
/// `img` encoded as `format`.
|
||||
fn encoded(img: image::DynamicImage, format: image::ImageFormat) -> Vec<u8> {
|
||||
let mut out = vec![];
|
||||
img.write_to(&mut std::io::Cursor::new(&mut out), format).unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_alpha_reads_the_alpha_of_formats_that_have_one() {
|
||||
let px = |x: u32, y: u32| image::Rgba([10, 20, 30, u8::try_from((x + 4 * y) * 16).unwrap()]);
|
||||
let rgba = image::DynamicImage::ImageRgba8(image::RgbaImage::from_fn(4, 4, px));
|
||||
let want: Vec<u8> = (0..16).map(|i| u8::try_from(i * 16).unwrap()).collect();
|
||||
for (name, img) in [
|
||||
("rgba", rgba.clone()),
|
||||
("gray and alpha", image::DynamicImage::ImageLumaA8(rgba.to_luma_alpha8())),
|
||||
("16 bits", image::DynamicImage::ImageRgba16(rgba.to_rgba16())),
|
||||
] {
|
||||
let got = decode_alpha(&encoded(img, image::ImageFormat::Png)).unwrap();
|
||||
assert_eq!(got, Some((4, 4, want.clone())), "{name}");
|
||||
}
|
||||
let rgb = image::DynamicImage::ImageRgb8(rgba.to_rgb8());
|
||||
assert_eq!(decode_alpha(&encoded(rgb.clone(), image::ImageFormat::Png)).unwrap(), None);
|
||||
assert_eq!(decode_alpha(&encoded(rgb, image::ImageFormat::Jpeg)).unwrap(), None);
|
||||
assert!(decode_alpha(b"not an image").is_err());
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ pub struct Raster {
|
||||
/// The pixels the first frame of GIF `bytes` declares in its image descriptor, which the decoder
|
||||
/// allocates for before it crops the frame to the logical screen the dimension check reads. `None`
|
||||
/// when the bytes aren't a GIF or hold no frame header.
|
||||
fn gif_first_frame_pixels(bytes: &[u8]) -> Option<u64> {
|
||||
pub(crate) fn gif_first_frame_pixels(bytes: &[u8]) -> Option<u64> {
|
||||
if !bytes.starts_with(b"GIF8") {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -190,6 +190,19 @@ pub fn run_with_progress(
|
||||
then(app, r)
|
||||
}
|
||||
|
||||
/// Run `f` on the UI thread once job `id` has finished, right after the job's own result is
|
||||
/// applied, with that applied result (an error when the work failed). Nothing runs when no job
|
||||
/// `id` is in progress.
|
||||
pub fn after(app: &mut VectorcraftApp, id: u64, f: impl FnOnce(&mut VectorcraftApp, &Result<Value, String>) + 'static) {
|
||||
let Some(job) = app.background.jobs.iter_mut().find(|j| j.id == id) else { return };
|
||||
let then = std::mem::replace(&mut job.then, Box::new(|_, r| r));
|
||||
job.then = Box::new(move |app, r| {
|
||||
let r = then(app, r);
|
||||
f(app, &r);
|
||||
r
|
||||
});
|
||||
}
|
||||
|
||||
/// Run `f` with every save and export it starts written at once, as with the preferences off, so
|
||||
/// its result is the written file or the error (a control client's `wait: true`). The jobs already
|
||||
/// running finish first, so none of them writes over its files afterwards.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
//! File → Package…: where the package folder goes, its name, and what it collects (the linked
|
||||
//! files, in a folder of their own or not, relinked or not; the fonts; the report). A document
|
||||
//! never saved is saved first (Save As); unsaved changes are saved before packaging. The web
|
||||
//! downloads the package as a zip.
|
||||
//! never saved is saved first (Save As); unsaved changes are saved before packaging, and when that
|
||||
//! save fails no package is written. The web downloads the package as a zip.
|
||||
//!
|
||||
//! Fields: `folder` (the location), `name` (the package folder), `copyLinks`, `linksFolder`,
|
||||
//! `relink`, `copyFonts`, `report`. OK runs `file.package` with them, then offers to show the
|
||||
//! package folder (`file.showPackage`). On the desktop with Background Export on, the package is
|
||||
//! collected and written on the background thread ([`crate::background`]) after the dialog closes.
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
@@ -108,21 +108,35 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
} else if d.str("folder").trim().is_empty() {
|
||||
return Err("choose a location for the package".into());
|
||||
}
|
||||
// The package holds the document as saved.
|
||||
// The package holds the document as saved: when the save fails, or asks a question and writes
|
||||
// nothing yet, no package is written.
|
||||
let mut saving = None;
|
||||
if app.session.active().is_some_and(|st| st.is_dirty()) {
|
||||
let r = io::save(app, vectorcraft_engine::cmd::fileio::SaveMode::Save, &json!({}), false)?;
|
||||
// The save asks first (it would replace a file that loses what opening it left out).
|
||||
if r.get("pending").is_some() {
|
||||
return Ok(r);
|
||||
}
|
||||
// Background Save: the save is written on the worker, and its result decides the package.
|
||||
if r["background"] == true {
|
||||
saving = app.background.jobs.last().map(background::Job::id);
|
||||
}
|
||||
}
|
||||
// Background Export (Preferences › File Handling): the linked files and fonts are collected
|
||||
// and the package is written from the document as it is now, while editing goes on. The jobs
|
||||
// started before it (a save started above, an earlier package to the same folder) are written
|
||||
// first: the worker runs its jobs in order, and a package made at once waits for them.
|
||||
// started before it (an earlier package to the same folder) are written first: the worker
|
||||
// runs its jobs in order, and a package made at once waits for them.
|
||||
let enabled = app.session.prefs.background_export;
|
||||
if !enabled {
|
||||
let outcome: Rc<RefCell<Option<Result<Value, String>>>> = Rc::default();
|
||||
if let Some(id) = saving {
|
||||
let applied = outcome.clone();
|
||||
background::after(app, id, move |_, r| *applied.borrow_mut() = Some(r.clone()));
|
||||
}
|
||||
background::wait_all(app);
|
||||
if let Some(r) = outcome.take().filter(|r| !saved(r)) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
let package = app.session.prepare_package(¶ms).map_err(|e| e.to_string()).inspect_err(|e| app.ui.status = e.clone())?;
|
||||
let label = format!("Packaging {}", d.str("name"));
|
||||
@@ -134,7 +148,23 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
|path: &str, bytes: &[u8]| write(path, bytes).map_err(|e| EngineError::Other(if e.contains(path) { e } else { format!("{path}: {e}") }));
|
||||
package.write(&mut write).map_err(|e| e.to_string())
|
||||
};
|
||||
let r = background::run(app, enabled, label, None, work, move |app, r| packaged(app, r, web, in_background.get()))?;
|
||||
let start =
|
||||
move |app: &mut VectorcraftApp| background::run(app, enabled, label, None, work, move |app, r| packaged(app, r, web, in_background.get()));
|
||||
// Both in the background: the package, collected from the document as it is now, starts once
|
||||
// the save is written.
|
||||
if let Some(id) = saving.filter(|_| enabled) {
|
||||
later.set(true);
|
||||
app.ui.dialog = None;
|
||||
background::after(app, id, move |app, r| {
|
||||
if saved(r)
|
||||
&& let Err(e) = start(app)
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
});
|
||||
return Ok(json!({ "background": true }));
|
||||
}
|
||||
let r = start(app)?;
|
||||
if r["background"] == true {
|
||||
later.set(true);
|
||||
app.ui.dialog = None;
|
||||
@@ -142,6 +172,12 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Did a save whose applied result is `r` write the document (no error, and no question asked
|
||||
/// before writing)?
|
||||
fn saved(r: &Result<Value, String>) -> bool {
|
||||
r.as_ref().is_ok_and(|r| r.get("pending").is_none())
|
||||
}
|
||||
|
||||
/// Report the package `r` (`file.package`'s reply): the status bar, the web's download, and the
|
||||
/// offer to show the package folder. `later`: it was written in the background, after the dialog
|
||||
/// closed, and a dialog open now is another one, which stays (the status bar names the folder).
|
||||
|
||||
@@ -181,7 +181,7 @@ fn a_dirty_documents_save_is_written_before_its_package() {
|
||||
let (mut app, files) = saved_with_worker(&dir, &gate);
|
||||
let (doc, out) = (dir.path("card.vectorcraft"), dir.path("out"));
|
||||
// With Background Export on, the save and the package both run in the background, and the
|
||||
// worker writes the save first.
|
||||
// package starts once the save is written.
|
||||
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
package_dialog(&mut app, &out);
|
||||
// A thread opens the gate after 10 s at the latest, so a confirm that waits for the save ends.
|
||||
@@ -191,7 +191,7 @@ fn a_dirty_documents_save_is_written_before_its_package() {
|
||||
opener.open();
|
||||
});
|
||||
assert_eq!(confirm(&mut app).unwrap()["background"], true);
|
||||
assert_eq!(app.background.jobs.len(), 2, "the save and the package");
|
||||
assert_eq!(app.background.jobs.len(), 1, "the save; the package waits for it");
|
||||
gate.open();
|
||||
background::wait_all(&mut app);
|
||||
let paths = written(&files);
|
||||
@@ -216,6 +216,38 @@ fn a_dirty_documents_save_is_written_before_its_package() {
|
||||
assert!(app.ui.status.starts_with("Packaged 3 files in "), "the package's status came last: {}", app.ui.status);
|
||||
}
|
||||
|
||||
/// A save before packaging that fails writes no package, with Background Export on or off.
|
||||
#[test]
|
||||
fn a_failed_save_before_packaging_writes_no_package() {
|
||||
let dir = Folder::new("failed-save");
|
||||
let (doc, out) = (dir.path("card.vectorcraft"), dir.path("out"));
|
||||
let root = std::path::Path::new(&out).join("card Folder");
|
||||
for background_export in [true, false] {
|
||||
let mut app = saved(&dir);
|
||||
let (files, refused) = (Files::default(), doc.clone());
|
||||
let recorded = files.clone();
|
||||
app.services.write_shared = Some(Arc::new(move |p: &str, b: &[u8]| {
|
||||
if p == refused {
|
||||
return Err("Access is denied. (os error 5)".into());
|
||||
}
|
||||
recorded.lock().unwrap().push((p.to_string(), b.to_vec()));
|
||||
Ok(())
|
||||
}));
|
||||
app.session.execute("prefs.set", &json!({"values": {"backgroundExport": background_export}})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
package_dialog(&mut app, &out);
|
||||
let r = confirm(&mut app);
|
||||
background::wait_all(&mut app);
|
||||
assert!(written(&files).is_empty(), "Background Export {background_export}: {:?}", written(&files));
|
||||
assert!(!root.exists(), "Background Export {background_export}: no package folder");
|
||||
assert!(app.session.active().unwrap().is_dirty(), "the save failed");
|
||||
match r {
|
||||
Ok(r) => assert!(background_export && r["background"] == true, "{r}"),
|
||||
Err(e) => assert!(!background_export && e.contains("Access is denied"), "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_package_made_at_once_waits_for_one_in_the_background() {
|
||||
let dir = Folder::new("twice");
|
||||
|
||||
@@ -659,7 +659,8 @@ const MESSAGE_DEPTH: usize = 5;
|
||||
|
||||
/// Translate a status or error message for display (see the module docs). Unknown messages, and
|
||||
/// parts of them (file names, numbers, a reason with no entry, a JSON object or array such as
|
||||
/// `{"objects": [...]}`), stay as they are. A part that starts with `{` or `[` is translated only
|
||||
/// `{"objects": [...]}`, a value the message quotes in `"…"`, `“…”` or backticks, such as a
|
||||
/// symbol's name), stay as they are. A part that starts with `{` or `[` is translated only
|
||||
/// when it is a whole message with an exact entry, such as `[x, y] (two numbers)`.
|
||||
pub fn message(lang: Lang, s: &str) -> String {
|
||||
message_at(lang, s, MESSAGE_DEPTH)
|
||||
@@ -670,8 +671,9 @@ fn message_at(lang: Lang, s: &str, depth: usize) -> String {
|
||||
let count = caps.iter().find(|(k, _)| *k == "n").and_then(|(_, v)| v.parse::<u64>().ok());
|
||||
let form = count.map_or(forms.len().saturating_sub(1), |n| (lang.0.plural)(n).min(forms.len().saturating_sub(1)));
|
||||
let Some(template) = forms.get(form) else { return s.to_string() };
|
||||
let quoted = |v: &str| [('"', '"'), ('“', '”'), ('`', '`')].iter().any(|(a, b)| s.contains(&format!("{a}{v}{b}")));
|
||||
let nested = |v: &str| {
|
||||
if depth <= 1 {
|
||||
if depth <= 1 || quoted(v) {
|
||||
v.to_string()
|
||||
} else if v.starts_with(['{', '[']) {
|
||||
lang.catalog().exact_message(v).unwrap_or(v).to_string()
|
||||
|
||||
@@ -2204,6 +2204,8 @@ font-technology Variable Променливи
|
||||
Normal Screen Mode Нормален режим на екран
|
||||
Note: Забележка:
|
||||
Nothing matches the Show filter. Нищо не отговаря на филтъра за показване.
|
||||
Show DCS Transparency Interactions Показване на взаимодействията на DCS с прозрачност
|
||||
Transparent, or overlaps transparent objects: check its separations Прозрачно е или се припокрива с прозрачни обекти: проверете цветоотделянето му
|
||||
Nothing to print Няма нищо за отпечатване
|
||||
Nudge Преместване
|
||||
Nudge in Perspective Преместване в перспектива
|
||||
@@ -5987,6 +5989,13 @@ axis Y Axis Y ос
|
||||
@msg Batch cannot play an action with a stop Пакет не може да изпълни действие със спиране
|
||||
Insert Menu Item… Вмъкване на елемент от меню…
|
||||
Insert Menu Item Вмъкване на елемент от меню
|
||||
Insert Select Path Вмъкване на избор на път
|
||||
Select Path Избор на път
|
||||
@msg select one whole path or compound path изберете един цял път или съставен път
|
||||
@msg select only one path изберете само един път
|
||||
@msg the path is hidden or locked, or cannot be selected here пътят е скрит или заключен, или не може да бъде избран тук
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one обект {value} не е запазеният път: той е променен или документът е друг
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` трябва да е "v1:" и 16 шестнадесетични цифри с малки букви
|
||||
Select Object… Избиране на обект…
|
||||
Select Object Избиране на обект
|
||||
Set Selection Задаване на селекция
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1929,6 +1929,8 @@ digits Malayalam Malajalam
|
||||
Modified since it was read: {path} Izmijenjeno nakon čitanja: {path}
|
||||
No Thumbnails Bez sličica
|
||||
Nothing matches the Show filter. Ništa ne odgovara filteru Prikaži.
|
||||
Show DCS Transparency Interactions Prikaži interakcije DCS-a s prozirnošću
|
||||
Transparent, or overlaps transparent objects: check its separations Prozirno je ili preklapa prozirne objekte: provjerite njegove separacije
|
||||
PPI PPI
|
||||
Relink… Ponovo poveži…
|
||||
Find Linked Files Pronađi povezane datoteke
|
||||
@@ -6029,6 +6031,13 @@ axis Y Axis Y osa
|
||||
@msg Batch cannot play an action with a stop Paketna obrada ne može reprodukovati akciju sa zaustavljanjem
|
||||
Insert Menu Item… Umetni stavku menija…
|
||||
Insert Menu Item Umetni stavku menija
|
||||
Insert Select Path Umetni odabir putanje
|
||||
Select Path Odabir putanje
|
||||
@msg select one whole path or compound path odaberite jednu cijelu putanju ili složenu putanju
|
||||
@msg select only one path odaberite samo jednu putanju
|
||||
@msg the path is hidden or locked, or cannot be selected here putanja je skrivena ili zaključana, ili se ovdje ne može odabrati
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one objekt {value} nije sačuvana putanja: promijenjena je ili je dokument drugi
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` mora biti "v1:" i 16 malih heksadecimalnih cifara
|
||||
Select Object… Odaberi objekt…
|
||||
Select Object Odaberi objekt
|
||||
Set Selection Postavi odabir
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2812,6 +2812,8 @@ digits Malayalam Malaiàlam
|
||||
Note: Nota:
|
||||
Nothing is selected. Select the art to export, then open this tab again. No hi ha res seleccionat. Selecciona la il·lustració que vols exportar i torna a obrir aquesta pestanya.
|
||||
Nothing matches the Show filter. Res no coincideix amb el filtre Mostra.
|
||||
Show DCS Transparency Interactions Mostra les interaccions de transparència DCS
|
||||
Transparent, or overlaps transparent objects: check its separations És transparent o se superposa a objectes transparents: comproveu-ne les separacions
|
||||
Nothing to print No hi ha res per imprimir
|
||||
NTSC Colors Colors NTSC
|
||||
Nudge Desplaça
|
||||
@@ -4041,6 +4043,13 @@ digits Malayalam Malaiàlam
|
||||
@msg Batch cannot play an action with a stop Lot no pot reproduir una acció amb una aturada
|
||||
Insert Menu Item… Insereix un element de menú…
|
||||
Insert Menu Item Insereix un element de menú
|
||||
Insert Select Path Insereix una selecció de traçat
|
||||
Select Path Selecciona el traçat
|
||||
@msg select one whole path or compound path selecciona un traçat sencer o un traçat compost
|
||||
@msg select only one path selecciona només un traçat
|
||||
@msg the path is hidden or locked, or cannot be selected here el traçat està amagat o bloquejat, o no es pot seleccionar aquí
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one l’objecte {value} no és el traçat desat: ha canviat o el document és un altre
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` ha de ser "v1:" i 16 dígits hexadecimals en minúscula
|
||||
Select Object… Selecciona un objecte…
|
||||
Select Object Selecciona un objecte
|
||||
Set Selection Defineix la selecció
|
||||
@@ -6026,6 +6035,65 @@ digits Malayalam Malaiàlam
|
||||
@msg Targa files are RGB: colorModel rgb Els fitxers Targa són RGB: colorModel rgb
|
||||
@msg Text Wrap: select a wrap object Ajustament de text: selecciona un objecte d’ajustament
|
||||
@msg that document is no longer open aquest document ja no està obert
|
||||
@msg Extrude & Bevel Extrudeix i bisella
|
||||
@msg Revolve Revoluciona
|
||||
@msg Inflate Infla
|
||||
@msg Rotate Gira
|
||||
@msg Extrude & Bevel (Classic) Extrudeix i bisella (clàssic)
|
||||
@msg Revolve (Classic) Revoluciona (clàssic)
|
||||
@msg Rotate (Classic) Gira (clàssic)
|
||||
@msg This effect Aquest efecte
|
||||
@msg clipping paths traçats de retall
|
||||
@msg other objects altres objectes
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} requereix un grup que contingui una forma amb com a mínim dos punts d’ancoratge
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Revoluciona requereix un grup que contingui una forma amb com a mínim dos punts d’ancoratge
|
||||
@msg Revolve requires a shape with at least two anchors Revoluciona requereix una forma amb com a mínim dos punts d’ancoratge
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} en un grup necessita traçats, traçats compostos, text, grups, grups de retall, fusions i instàncies de símbol: primer allibera o expandeix aquests elements: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} en un grup admet com a màxim {MAX_MEMBERS} objectes
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke Un efecte 3D en un grup o una capa s’aplica al mateix grup, no al seu farciment ni al seu traç
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing un efecte 3D al farciment o al traç d’un grup no dibuixa res
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un efecte 3D al farciment o al traç d’un grup no dibuixa res per exportar
|
||||
@msg The 3D object is too complex to export L’objecte 3D és massa complex per exportar-lo
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) l’objecte no té cap efecte 3D (Extrudeix i bisella, Revoluciona, Infla o Gira)
|
||||
@msg its 3D effect builds no solid el seu efecte 3D no construeix cap sòlid
|
||||
@msg bevel must be none or one of {_1} bevel ha de ser none o un de {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} no pot construir sòlids a partir d’aquesta il·lustració: les seves instàncies de símbol, fusions i grups de retall contenen massa objectes
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} no pot construir un sòlid a partir de la instància del símbol «{symbol}»: la il·lustració del símbol no està disponible
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: el traçat de retall és massa complex per retallar-hi la il·lustració
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} admet com a màxim {MAX_MEMBERS} objectes
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} no pot construir sòlids a partir d’una il·lustració imbricada en més de {MAX_DEPTH} nivells
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient La pintura de Revoluciona és massa complexa per a aquesta superfície; redueix Segments o simplifica el degradat
|
||||
@msg Revolve edge must be left or right Revoluciona: edge ha de ser left o right
|
||||
@msg Revolve cap must be a boolean Revoluciona: cap ha de ser un booleà
|
||||
@msg the revolved object has no fill or stroke l’objecte de revolució no té farciment ni traç
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor ha de ser un color RGB opac en format #rrggbb
|
||||
@msg lights must contain one to ten lights lights ha de contenir d’un a deu llums
|
||||
@msg each light must be an object cada llum ha de ser un objecte
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind paràmetre de llum desconegut: fes servir color, intensity, rotation, height, softness o behind
|
||||
@msg light {key} must be a number from {min} to {max} llum: {key} ha de ser un nombre de {min} a {max}
|
||||
@msg light color must use opaque #rrggbb color d’un llum ha de fer servir #rrggbb opac
|
||||
@msg light behind must be true or false behind d’un llum ha de ser true o false
|
||||
@msg shadow must be true or false shadow ha de ser true o false
|
||||
@msg shadowPosition must be behind or below shadowPosition ha de ser behind o below
|
||||
@msg {key} must be a number from {min} to {max} {key} ha de ser un nombre de {min} a {max}
|
||||
@msg each of `{GRAPHICS}` must be an object cada element de `{GRAPHICS}` ha de ser un objecte
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` les entrades de `{GRAPHICS}` admeten symbol, face, x, y, scale i rotation, no `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol cada element de `{GRAPHICS}` necessita `symbol`, el nom d’un símbol
|
||||
@msg `{GRAPHICS}` face must be front or back face de `{GRAPHICS}` ha de ser front o back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} {key} de `{GRAPHICS}` ha de ser un nombre de {lo} a {hi}
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` ha de ser una matriu
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Els gràfics s’apliquen a cares planes: fes servir el tipus d’objecte Extrusió o Pla
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` admet com a màxim {MAX_GRAPHICS} gràfics
|
||||
@msg A plane has one face: map its graphics onto the front Un pla té una sola cara: aplica’n els gràfics a la cara davantera
|
||||
@msg The graphics are too complex: simplify the symbols' art Els gràfics són massa complexos: simplifica la il·lustració dels símbols
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art La il·lustració d’un gràfic es pot imbricar com a màxim en {MAX_LEVELS} nivells: simplifica la il·lustració del símbol
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art La il·lustració d’un gràfic pot imbricar símbols com a màxim en {MAX_NESTING} nivells: simplifica la il·lustració del símbol
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art La il·lustració d’un gràfic conté massa objectes: simplifica la il·lustració del símbol
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Un gràfic dibuixa com a màxim {MAX_PIECES} farciments i traços: simplifica la il·lustració del símbol
|
||||
@msg `surface` must be one of {_1} `surface` ha de ser un de {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} dibuixa la il·lustració d’origen d’aquest objecte en lloc de la seva superfície 3D
|
||||
@msg {label} draws no faces for this object {label} no dibuixa cap cara per a aquest objecte
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges el filferro té més de {MAX_WIRE_EDGES} arestes
|
||||
@msg The 3D surface is too complex; simplify the shape La superfície 3D és massa complexa; simplifica la forma
|
||||
@msg the action has no steps l’acció no té cap pas
|
||||
@msg the action has no included steps l’acció no té cap pas inclòs
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1960,6 +1960,8 @@ digits Malayalam Malajálamské
|
||||
Modified since it was read: {path} Upraveno od načtení: {path}
|
||||
No Thumbnails Bez miniatur
|
||||
Nothing matches the Show filter. Filtru Zobrazit nic neodpovídá.
|
||||
Show DCS Transparency Interactions Zobrazit interakce DCS s průhledností
|
||||
Transparent, or overlaps transparent objects: check its separations Je průhledný nebo překrývá průhledné objekty: zkontrolujte jeho separace
|
||||
PPI PPI
|
||||
Relink… Znovu propojit…
|
||||
Find Linked Files Najít propojené soubory
|
||||
@@ -5876,6 +5878,65 @@ mark-style Japanese Japonské
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} nedokáže vytvořit těleso z obrazu: nejprve jej vektorizujte příkazem Objekt › Vektorizace obrazu › Vytvořit.
|
||||
@msg {label} requires a shape with at least two anchors {label} vyžaduje tvar alespoň se dvěma kotevními body
|
||||
@msg The shape is too complex for a 3D effect Tvar je pro 3D efekt příliš složitý
|
||||
@msg Extrude & Bevel Vysunutí a zkosení
|
||||
@msg Revolve Rotační těleso
|
||||
@msg Inflate Nafouknutí
|
||||
@msg Rotate Otočit
|
||||
@msg Extrude & Bevel (Classic) Vysunutí a zkosení (klasické)
|
||||
@msg Revolve (Classic) Rotační těleso (klasické)
|
||||
@msg Rotate (Classic) Otočit (klasické)
|
||||
@msg This effect Tento efekt
|
||||
@msg clipping paths ořezové cesty
|
||||
@msg other objects jiné objekty
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} vyžaduje skupinu obsahující tvar alespoň se dvěma kotevními body
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Rotační těleso vyžaduje skupinu obsahující tvar alespoň se dvěma kotevními body
|
||||
@msg Revolve requires a shape with at least two anchors Rotační těleso vyžaduje tvar alespoň se dvěma kotevními body
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} na skupině potřebuje cesty, složené cesty, text, skupiny, ořezové skupiny, prolnutí a instance symbolů: nejprve uvolněte nebo rozdělte tyto položky: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} na skupině přijímá nejvýše {MAX_MEMBERS} objektů
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke 3D efekt na skupině nebo vrstvě patří na samotnou skupinu, ne na její výplň nebo tah
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing 3D efekt na výplni nebo tahu skupiny nic nekreslí
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 3D efekt na výplni nebo tahu skupiny nekreslí nic k exportu
|
||||
@msg The 3D object is too complex to export 3D objekt je pro export příliš složitý
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) objekt nemá žádný 3D efekt (Vysunutí a zkosení, Rotační těleso, Nafouknutí ani Otočit)
|
||||
@msg its 3D effect builds no solid jeho 3D efekt nevytváří žádné těleso
|
||||
@msg bevel must be none or one of {_1} bevel musí být none nebo jedno z {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} nemůže z této kresby vytvořit tělesa: její instance symbolů, prolnutí a ořezové skupiny obsahují příliš mnoho objektů
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} nemůže vytvořit těleso z instance symbolu „{symbol}“: kresba symbolu není k dispozici
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: ořezová cesta je příliš složitá na oříznutí kresby
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} přijímá nejvýše {MAX_MEMBERS} objektů
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} nemůže vytvořit tělesa z kresby vnořené hlouběji než do {MAX_DEPTH} úrovní
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient Barva rotačního tělesa je pro tento povrch příliš složitá; snižte Segmenty nebo zjednodušte přechod
|
||||
@msg Revolve edge must be left or right Rotační těleso: edge musí být left nebo right
|
||||
@msg Revolve cap must be a boolean Rotační těleso: cap musí být logická hodnota
|
||||
@msg the revolved object has no fill or stroke rotovaný objekt nemá výplň ani tah
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor musí být neprůhledná barva RGB ve tvaru #rrggbb
|
||||
@msg lights must contain one to ten lights lights musí obsahovat jedno až deset světel
|
||||
@msg each light must be an object každé světlo musí být objekt
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind neznámé nastavení světla: použijte color, intensity, rotation, height, softness nebo behind
|
||||
@msg light {key} must be a number from {min} to {max} světlo: {key} musí být číslo od {min} do {max}
|
||||
@msg light color must use opaque #rrggbb color světla musí být neprůhledná barva #rrggbb
|
||||
@msg light behind must be true or false behind světla musí být true nebo false
|
||||
@msg shadow must be true or false shadow musí být true nebo false
|
||||
@msg shadowPosition must be behind or below shadowPosition musí být behind nebo below
|
||||
@msg {key} must be a number from {min} to {max} {key} musí být číslo od {min} do {max}
|
||||
@msg each of `{GRAPHICS}` must be an object každá položka `{GRAPHICS}` musí být objekt
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` položky `{GRAPHICS}` přijímají symbol, face, x, y, scale a rotation, ne `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol každá položka `{GRAPHICS}` potřebuje `symbol`, název symbolu
|
||||
@msg `{GRAPHICS}` face must be front or back face v `{GRAPHICS}` musí být front nebo back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} {key} v `{GRAPHICS}` musí být číslo od {lo} do {hi}
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` musí být pole
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Grafika se mapuje na rovinné stěny: použijte typ objektu Vysunutí nebo Rovina
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` přijímá nejvýše {MAX_GRAPHICS} grafik
|
||||
@msg A plane has one face: map its graphics onto the front Rovina má jednu stranu: umístěte její grafiku na přední stranu
|
||||
@msg The graphics are too complex: simplify the symbols' art Grafika je příliš složitá: zjednodušte kresbu symbolů
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art Kresba grafiky smí být vnořena nejvýše do {MAX_LEVELS} úrovní: zjednodušte kresbu symbolu
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art Kresba grafiky smí vnořovat symboly nejvýše do {MAX_NESTING} úrovní: zjednodušte kresbu symbolu
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art Kresba grafiky obsahuje příliš mnoho objektů: zjednodušte kresbu symbolu
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Grafika kreslí nejvýše {MAX_PIECES} výplní a tahů: zjednodušte kresbu symbolu
|
||||
@msg `surface` must be one of {_1} `surface` musí být jedno z {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} pro tento objekt kreslí zdrojovou kresbu místo jeho 3D povrchu
|
||||
@msg {label} draws no faces for this object {label} pro tento objekt nekreslí žádné stěny
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges drátový model má více než {MAX_WIRE_EDGES} hran
|
||||
@msg The 3D surface is too complex; simplify the shape 3D povrch je příliš složitý; zjednodušte tvar
|
||||
Transform Objects Transformovat objekty
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? neznámý parametr `{key}`; mysleli jste `{near}`?
|
||||
@@ -6461,6 +6522,13 @@ axis Y Axis osa Y
|
||||
@msg Batch cannot play an action with a stop Dávka nemůže přehrát akci se zastavením
|
||||
Insert Menu Item… Vložit položku nabídky…
|
||||
Insert Menu Item Vložit položku nabídky
|
||||
Insert Select Path Vložit výběr cesty
|
||||
Select Path Vybrat cestu
|
||||
@msg select one whole path or compound path vyberte jednu celou cestu nebo složenou cestu
|
||||
@msg select only one path vyberte jen jednu cestu
|
||||
@msg the path is hidden or locked, or cannot be selected here cesta je skrytá nebo zamčená, nebo ji zde nelze vybrat
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one objekt {value} není uložená cesta: změnila se, nebo jde o jiný dokument
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` musí být "v1:" a 16 šestnáctkových číslic malými písmeny
|
||||
Select Object… Vybrat objekt…
|
||||
Select Object Vybrat objekt
|
||||
Set Selection Nastavit výběr
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1775,6 +1775,8 @@ digits Malayalam Malayalam
|
||||
Modified since it was read: {path} Ændret siden den blev læst: {path}
|
||||
No Thumbnails Ingen miniaturer
|
||||
Nothing matches the Show filter. Intet matcher Vis-filteret.
|
||||
Show DCS Transparency Interactions Vis DCS-gennemsigtighedsinteraktioner
|
||||
Transparent, or overlaps transparent objects: check its separations Gennemsigtig eller overlapper gennemsigtige objekter: kontrollér dens separationer
|
||||
PPI PPI
|
||||
Relink… Sammenkæd igen…
|
||||
Find Linked Files Find sammenkædede filer
|
||||
@@ -6004,6 +6006,13 @@ axis Y Axis Y-akse
|
||||
@msg Batch cannot play an action with a stop Batch kan ikke afspille en handling med et stop
|
||||
Insert Menu Item… Indsæt menupunkt…
|
||||
Insert Menu Item Indsæt menupunkt
|
||||
Insert Select Path Indsæt kurvemarkering
|
||||
Select Path Markér kurve
|
||||
@msg select one whole path or compound path markér én hel kurve eller sammensat kurve
|
||||
@msg select only one path markér kun én kurve
|
||||
@msg the path is hidden or locked, or cannot be selected here kurven er skjult eller låst, eller kan ikke markeres her
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one objekt {value} er ikke den gemte kurve: den er ændret, eller dokumentet er et andet
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` skal være "v1:" og 16 hexadecimale cifre med små bogstaver
|
||||
Select Object… Markér objekt…
|
||||
Select Object Markér objekt
|
||||
Set Selection Angiv markering
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2238,6 +2238,8 @@ file-filter Templates Vorlagen
|
||||
Normal Screen Mode Normaler Bildschirmmodus
|
||||
Note: Notiz:
|
||||
Nothing matches the Show filter. Nichts entspricht dem Filter „Anzeigen“.
|
||||
Show DCS Transparency Interactions DCS-Transparenzinteraktionen anzeigen
|
||||
Transparent, or overlaps transparent objects: check its separations Transparent oder überlappt transparente Objekte: Separationen prüfen
|
||||
Nothing to print Nichts zu drucken
|
||||
Nudge Schrittweise verschieben
|
||||
Nudge in Perspective In Perspektive schrittweise verschieben
|
||||
@@ -5451,6 +5453,65 @@ lighting Light Licht
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} kann aus einem Bild keinen Körper erstellen: Zeichnen Sie es zuerst mit Objekt › Bildnachzeichner › Erstellen nach.
|
||||
@msg {label} requires a shape with at least two anchors {label} erfordert eine Form mit mindestens zwei Ankerpunkten
|
||||
@msg The shape is too complex for a 3D effect Die Form ist für einen 3D-Effekt zu komplex
|
||||
@msg Extrude & Bevel Extrudieren und abgeflachte Kante
|
||||
@msg Revolve Kreiseln
|
||||
@msg Inflate Aufblasen
|
||||
@msg Rotate Drehen
|
||||
@msg Extrude & Bevel (Classic) Extrudieren und abgeflachte Kante (klassisch)
|
||||
@msg Revolve (Classic) Kreiseln (klassisch)
|
||||
@msg Rotate (Classic) Drehen (klassisch)
|
||||
@msg This effect Dieser Effekt
|
||||
@msg clipping paths Schnittpfade
|
||||
@msg other objects andere Objekte
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} erfordert eine Gruppe mit einer Form mit mindestens zwei Ankerpunkten
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Kreiseln erfordert eine Gruppe mit einer Form mit mindestens zwei Ankerpunkten
|
||||
@msg Revolve requires a shape with at least two anchors Kreiseln erfordert eine Form mit mindestens zwei Ankerpunkten
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} auf einer Gruppe braucht Pfade, zusammengesetzte Pfade, Text, Gruppen, Schnittgruppen, Angleichungen und Symbolinstanzen: Geben Sie zuerst Folgendes frei oder wandeln Sie es um: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} auf einer Gruppe nimmt höchstens {MAX_MEMBERS} Objekte auf
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke Ein 3D-Effekt auf einer Gruppe oder Ebene gehört auf die Gruppe selbst, nicht auf ihre Fläche oder Kontur
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing ein 3D-Effekt auf der Fläche oder Kontur einer Gruppe zeichnet nichts
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ein 3D-Effekt auf der Fläche oder Kontur einer Gruppe zeichnet nichts, das exportiert werden kann
|
||||
@msg The 3D object is too complex to export Das 3D-Objekt ist zu komplex für den Export
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) das Objekt hat keinen 3D-Effekt (Extrudieren und abgeflachte Kante, Kreiseln, Aufblasen oder Drehen)
|
||||
@msg its 3D effect builds no solid sein 3D-Effekt erzeugt keinen Körper
|
||||
@msg bevel must be none or one of {_1} bevel muss none oder einer der folgenden Werte sein: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} kann aus diesem Bildmaterial keine Körper erstellen: Seine Symbolinstanzen, Angleichungen und Schnittgruppen enthalten zu viele Objekte
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} kann keinen Körper aus der Instanz des Symbols „{symbol}“ erstellen: Das Bildmaterial des Symbols ist nicht verfügbar
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: Der Schnittpfad ist zu komplex, um das Bildmaterial damit zu beschneiden
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} nimmt höchstens {MAX_MEMBERS} Objekte auf
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} kann keine Körper aus Bildmaterial erstellen, das mehr als {MAX_DEPTH} Ebenen tief verschachtelt ist
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient Die Farbe von Kreiseln ist für diese Oberfläche zu komplex; verringern Sie Segmente oder vereinfachen Sie den Verlauf
|
||||
@msg Revolve edge must be left or right Kreiseln: edge muss left oder right sein
|
||||
@msg Revolve cap must be a boolean Kreiseln: cap muss ein boolescher Wert sein
|
||||
@msg the revolved object has no fill or stroke das gekreiselte Objekt hat keine Fläche oder Kontur
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor muss eine deckende RGB-Farbe der Form #rrggbb sein
|
||||
@msg lights must contain one to ten lights lights muss ein bis zehn Lichter enthalten
|
||||
@msg each light must be an object jedes Licht muss ein Objekt sein
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind unbekannte Lichteinstellung: Verwenden Sie color, intensity, rotation, height, softness oder behind
|
||||
@msg light {key} must be a number from {min} to {max} Licht: {key} muss eine Zahl von {min} bis {max} sein
|
||||
@msg light color must use opaque #rrggbb color eines Lichts muss ein deckendes #rrggbb sein
|
||||
@msg light behind must be true or false behind eines Lichts muss true oder false sein
|
||||
@msg shadow must be true or false shadow muss true oder false sein
|
||||
@msg shadowPosition must be behind or below shadowPosition muss behind oder below sein
|
||||
@msg {key} must be a number from {min} to {max} {key} muss eine Zahl von {min} bis {max} sein
|
||||
@msg each of `{GRAPHICS}` must be an object jeder Eintrag von `{GRAPHICS}` muss ein Objekt sein
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` `{GRAPHICS}`-Einträge nehmen symbol, face, x, y, scale und rotation an, nicht `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol jeder Eintrag von `{GRAPHICS}` braucht `symbol`, den Namen eines Symbols
|
||||
@msg `{GRAPHICS}` face must be front or back face in `{GRAPHICS}` muss front oder back sein
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} {key} in `{GRAPHICS}` muss eine Zahl von {lo} bis {hi} sein
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` muss ein Array sein
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Grafiken werden auf ebene Seiten abgebildet: Verwenden Sie den Objekttyp Extrudieren oder Ebene
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` nimmt höchstens {MAX_GRAPHICS} Grafiken auf
|
||||
@msg A plane has one face: map its graphics onto the front Eine Ebene hat nur eine Seite: Bilden Sie ihre Grafiken auf der Vorderseite ab
|
||||
@msg The graphics are too complex: simplify the symbols' art Die Grafiken sind zu komplex: Vereinfachen Sie das Bildmaterial der Symbole
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art Das Bildmaterial einer Grafik darf höchstens {MAX_LEVELS} Ebenen tief verschachtelt sein: Vereinfachen Sie das Bildmaterial des Symbols
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art Das Bildmaterial einer Grafik darf Symbole höchstens {MAX_NESTING} Ebenen tief verschachteln: Vereinfachen Sie das Bildmaterial des Symbols
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art Das Bildmaterial einer Grafik enthält zu viele Objekte: Vereinfachen Sie das Bildmaterial des Symbols
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Eine Grafik zeichnet höchstens {MAX_PIECES} Flächen und Konturen: Vereinfachen Sie das Bildmaterial des Symbols
|
||||
@msg `surface` must be one of {_1} `surface` muss einer der folgenden Werte sein: {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} zeichnet für dieses Objekt das Ausgangsbildmaterial statt seiner 3D-Oberfläche
|
||||
@msg {label} draws no faces for this object {label} zeichnet für dieses Objekt keine Seiten
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges das Drahtmodell hat mehr als {MAX_WIRE_EDGES} Kanten
|
||||
@msg The 3D surface is too complex; simplify the shape Die 3D-Oberfläche ist zu komplex; vereinfachen Sie die Form
|
||||
Transform Objects Objekte transformieren
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? unbekannter Parameter `{key}`; meinten Sie `{near}`?
|
||||
@@ -6052,6 +6113,13 @@ axis Y Axis Y-Achse
|
||||
@msg Batch cannot play an action with a stop Die Stapelverarbeitung kann keine Aktion mit einem Stopp abspielen
|
||||
Insert Menu Item… Menübefehl einfügen…
|
||||
Insert Menu Item Menübefehl einfügen
|
||||
Insert Select Path Pfadauswahl einfügen
|
||||
Select Path Pfad auswählen
|
||||
@msg select one whole path or compound path wählen Sie einen ganzen Pfad oder zusammengesetzten Pfad aus
|
||||
@msg select only one path wählen Sie nur einen Pfad aus
|
||||
@msg the path is hidden or locked, or cannot be selected here der Pfad ist ausgeblendet oder gesperrt oder kann hier nicht ausgewählt werden
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one Objekt {value} ist nicht der gespeicherte Pfad: Er wurde geändert, oder es ist ein anderes Dokument
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` muss aus "v1:" und 16 hexadezimalen Ziffern in Kleinbuchstaben bestehen
|
||||
Select Object… Objekt auswählen…
|
||||
Select Object Objekt auswählen
|
||||
Set Selection Auswahl festlegen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3136,6 +3136,8 @@ theme Appearance Εμφάνιση
|
||||
Normal Screen Mode Κανονική λειτουργία οθόνης
|
||||
Note: Σημείωση:
|
||||
Nothing matches the Show filter. Κανένα στοιχείο δεν αντιστοιχεί στο φίλτρο Εμφάνιση.
|
||||
Show DCS Transparency Interactions Εμφάνιση αλληλεπιδράσεων διαφάνειας DCS
|
||||
Transparent, or overlaps transparent objects: check its separations Διαφανές ή επικαλύπτει διαφανή αντικείμενα: ελέγξτε τους διαχωρισμούς του
|
||||
Nothing to print Δεν υπάρχει τίποτα για εκτύπωση
|
||||
Nudge Μετακίνηση κατά βήμα
|
||||
Nudge in Perspective Μετακίνηση κατά βήμα σε προοπτική
|
||||
@@ -5235,6 +5237,65 @@ digits Malayalam Μαλαγιάλαμ
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. Το {label} δεν μπορεί να δημιουργήσει στερεό από εικόνα: ανιχνεύστε την πρώτα με Αντικείμενο › Ανίχνευση περιγραμμάτων εικόνας › Κατασκευαστής.
|
||||
@msg {label} requires a shape with at least two anchors το {label} απαιτεί σχήμα με τουλάχιστον δύο σημεία αγκύρωσης
|
||||
@msg The shape is too complex for a 3D effect Το σχήμα είναι πολύ σύνθετο για εφέ 3D
|
||||
@msg Extrude & Bevel Εξώθηση και λοξότμηση
|
||||
@msg Revolve Περιστροφή γύρω από άξονα
|
||||
@msg Inflate Φούσκωμα
|
||||
@msg Rotate Περιστροφή
|
||||
@msg Extrude & Bevel (Classic) Εξώθηση και λοξότμηση (κλασικό)
|
||||
@msg Revolve (Classic) Περιστροφή γύρω από άξονα (κλασικό)
|
||||
@msg Rotate (Classic) Περιστροφή (κλασικό)
|
||||
@msg This effect Αυτό το εφέ
|
||||
@msg clipping paths διαδρομές αποκοπής
|
||||
@msg other objects άλλα αντικείμενα
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label}: απαιτείται ομάδα με σχήμα με τουλάχιστον δύο σημεία αγκύρωσης
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Περιστροφή γύρω από άξονα: απαιτείται ομάδα με σχήμα με τουλάχιστον δύο σημεία αγκύρωσης
|
||||
@msg Revolve requires a shape with at least two anchors Περιστροφή γύρω από άξονα: απαιτείται σχήμα με τουλάχιστον δύο σημεία αγκύρωσης
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} σε ομάδα: απαιτούνται διαδρομές, σύνθετες διαδρομές, κείμενο, ομάδες, ομάδες αποκοπής, αναμείξεις και παρουσίες συμβόλων· πρώτα αποδεσμεύστε ή αναπτύξτε τα εξής: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} σε ομάδα: το πολύ {MAX_MEMBERS} αντικείμενα
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke Ένα εφέ 3D σε ομάδα ή επίπεδο εφαρμόζεται στην ίδια την ομάδα, όχι στο γέμισμα ή στην πινελιά της
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing ένα εφέ 3D στο γέμισμα ή στην πινελιά μιας ομάδας δεν σχεδιάζει τίποτα
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ένα εφέ 3D στο γέμισμα ή στην πινελιά μιας ομάδας δεν σχεδιάζει τίποτα για εξαγωγή
|
||||
@msg The 3D object is too complex to export Το αντικείμενο 3D είναι πολύ σύνθετο για εξαγωγή
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) το αντικείμενο δεν έχει εφέ 3D (Εξώθηση και λοξότμηση, Περιστροφή γύρω από άξονα, Φούσκωμα ή Περιστροφή)
|
||||
@msg its 3D effect builds no solid το εφέ 3D του δεν δημιουργεί στερεό
|
||||
@msg bevel must be none or one of {_1} το bevel πρέπει να είναι none ή ένα από τα {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: δεν μπορούν να δημιουργηθούν στερεά από αυτό το σχέδιο· οι παρουσίες συμβόλων, οι αναμείξεις και οι ομάδες αποκοπής του περιέχουν πάρα πολλά αντικείμενα
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label}: δεν μπορεί να δημιουργηθεί στερεό από την παρουσία του συμβόλου «{symbol}»· το σχέδιο του συμβόλου δεν είναι διαθέσιμο
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: η διαδρομή αποκοπής είναι πολύ σύνθετη για να κόψει το σχέδιο
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1}: το πολύ {MAX_MEMBERS} αντικείμενα
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1}: δεν μπορούν να δημιουργηθούν στερεά από σχέδιο ένθετο σε περισσότερα από {MAX_DEPTH} επίπεδα
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient Το χρώμα της Περιστροφής γύρω από άξονα είναι πολύ σύνθετο για αυτή την επιφάνεια· μειώστε τα Τμήματα ή απλοποιήστε τη διαβάθμιση
|
||||
@msg Revolve edge must be left or right Περιστροφή γύρω από άξονα: το edge πρέπει να είναι left ή right
|
||||
@msg Revolve cap must be a boolean Περιστροφή γύρω από άξονα: το cap πρέπει να είναι λογική τιμή
|
||||
@msg the revolved object has no fill or stroke το περιστρεφόμενο αντικείμενο δεν έχει γέμισμα ούτε πινελιά
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form το lightColor πρέπει να είναι αδιαφανές χρώμα RGB της μορφής #rrggbb
|
||||
@msg lights must contain one to ten lights το lights πρέπει να περιέχει από ένα έως δέκα φώτα
|
||||
@msg each light must be an object κάθε φως πρέπει να είναι αντικείμενο
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind άγνωστη ρύθμιση φωτός: χρησιμοποιήστε color, intensity, rotation, height, softness ή behind
|
||||
@msg light {key} must be a number from {min} to {max} φως: το {key} πρέπει να είναι αριθμός από {min} έως {max}
|
||||
@msg light color must use opaque #rrggbb το color ενός φωτός πρέπει να είναι αδιαφανές #rrggbb
|
||||
@msg light behind must be true or false το behind ενός φωτός πρέπει να είναι true ή false
|
||||
@msg shadow must be true or false το shadow πρέπει να είναι true ή false
|
||||
@msg shadowPosition must be behind or below το shadowPosition πρέπει να είναι behind ή below
|
||||
@msg {key} must be a number from {min} to {max} το {key} πρέπει να είναι αριθμός από {min} έως {max}
|
||||
@msg each of `{GRAPHICS}` must be an object κάθε στοιχείο του `{GRAPHICS}` πρέπει να είναι αντικείμενο
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` οι καταχωρίσεις του `{GRAPHICS}` δέχονται symbol, face, x, y, scale και rotation, όχι `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol κάθε στοιχείο του `{GRAPHICS}` χρειάζεται `symbol`, το όνομα ενός συμβόλου
|
||||
@msg `{GRAPHICS}` face must be front or back το face του `{GRAPHICS}` πρέπει να είναι front ή back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} το {key} του `{GRAPHICS}` πρέπει να είναι αριθμός από {lo} έως {hi}
|
||||
@msg `{GRAPHICS}` must be an array το `{GRAPHICS}` πρέπει να είναι πίνακας
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Τα γραφικά αντιστοιχίζονται σε επίπεδες όψεις: χρησιμοποιήστε τον τύπο αντικειμένου Εξώθηση ή Επίπεδο
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics το `{GRAPHICS}` δέχεται το πολύ {MAX_GRAPHICS} γραφικά
|
||||
@msg A plane has one face: map its graphics onto the front Ένα επίπεδο έχει μία όψη: αντιστοιχίστε τα γραφικά του στην μπροστινή όψη
|
||||
@msg The graphics are too complex: simplify the symbols' art Τα γραφικά είναι πολύ σύνθετα: απλοποιήστε το σχέδιο των συμβόλων
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art Το σχέδιο ενός γραφικού μπορεί να είναι ένθετο το πολύ σε {MAX_LEVELS} επίπεδα: απλοποιήστε το σχέδιο του συμβόλου
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art Το σχέδιο ενός γραφικού μπορεί να περιέχει ένθετα σύμβολα το πολύ σε {MAX_NESTING} επίπεδα: απλοποιήστε το σχέδιο του συμβόλου
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art Το σχέδιο ενός γραφικού περιέχει πάρα πολλά αντικείμενα: απλοποιήστε το σχέδιο του συμβόλου
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Ένα γραφικό σχεδιάζει το πολύ {MAX_PIECES} γεμίσματα και πινελιές: απλοποιήστε το σχέδιο του συμβόλου
|
||||
@msg `surface` must be one of {_1} Το `surface` πρέπει να είναι ένα από τα {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label}: σχεδιάζεται το αρχικό σχέδιο αυτού του αντικειμένου αντί για την επιφάνεια 3D του
|
||||
@msg {label} draws no faces for this object {label}: δεν σχεδιάζεται καμία όψη για αυτό το αντικείμενο
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges το συρμάτινο πλέγμα έχει περισσότερες από {MAX_WIRE_EDGES} ακμές
|
||||
@msg The 3D surface is too complex; simplify the shape Η επιφάνεια 3D είναι πολύ σύνθετη· απλοποιήστε το σχήμα
|
||||
Transform Objects Μετασχηματισμός αντικειμένων
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? άγνωστη παράμετρος `{key}`· μήπως εννοείτε `{near}`;
|
||||
@@ -6412,6 +6473,13 @@ axis Y Axis Άξονας Y
|
||||
@msg Batch cannot play an action with a stop Η μαζική επεξεργασία δεν μπορεί να αναπαράγει ενέργεια με διακοπή
|
||||
Insert Menu Item… Εισαγωγή στοιχείου μενού…
|
||||
Insert Menu Item Εισαγωγή στοιχείου μενού
|
||||
Insert Select Path Εισαγωγή επιλογής διαδρομής
|
||||
Select Path Επιλογή διαδρομής
|
||||
@msg select one whole path or compound path επιλέξτε μία ολόκληρη διαδρομή ή σύνθετη διαδρομή
|
||||
@msg select only one path επιλέξτε μόνο μία διαδρομή
|
||||
@msg the path is hidden or locked, or cannot be selected here η διαδρομή είναι κρυφή ή κλειδωμένη, ή δεν μπορεί να επιλεγεί εδώ
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one το αντικείμενο {value} δεν είναι η αποθηκευμένη διαδρομή: άλλαξε ή το έγγραφο είναι άλλο
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits το `fingerprint` πρέπει να είναι "v1:" και 16 πεζά δεκαεξαδικά ψηφία
|
||||
Select Object… Επιλογή αντικειμένου…
|
||||
Select Object Επιλογή αντικειμένου
|
||||
Set Selection Ορισμός επιλογής
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2276,6 +2276,8 @@ font-technology Variable Variables
|
||||
Normal Screen Mode Modo de pantalla normal
|
||||
Note: Nota:
|
||||
Nothing matches the Show filter. Nada coincide con el filtro Mostrar.
|
||||
Show DCS Transparency Interactions Mostrar interacciones de transparencia DCS
|
||||
Transparent, or overlaps transparent objects: check its separations Transparente o superpuesto a objetos transparentes: compruebe sus separaciones
|
||||
Nothing to print No hay nada para imprimir
|
||||
Nudge Desplazar
|
||||
Nudge in Perspective Desplazar en perspectiva
|
||||
@@ -5817,6 +5819,65 @@ mark-style Japanese Japonesas
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} no puede crear un sólido a partir de una imagen: cálquela primero con Objeto › Calco de imagen › Crear.
|
||||
@msg {label} requires a shape with at least two anchors {label} requiere una forma con al menos dos puntos de ancla
|
||||
@msg The shape is too complex for a 3D effect La forma es demasiado compleja para un efecto 3D
|
||||
@msg Extrude & Bevel Extrusión y biselado
|
||||
@msg Revolve Revolución
|
||||
@msg Inflate Inflar
|
||||
@msg Rotate Rotar
|
||||
@msg Extrude & Bevel (Classic) Extrusión y biselado (clásico)
|
||||
@msg Revolve (Classic) Revolución (clásico)
|
||||
@msg Rotate (Classic) Rotar (clásico)
|
||||
@msg This effect Este efecto
|
||||
@msg clipping paths trazados de recorte
|
||||
@msg other objects otros objetos
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} requiere un grupo que contenga una forma con al menos dos puntos de ancla
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Revolución requiere un grupo que contenga una forma con al menos dos puntos de ancla
|
||||
@msg Revolve requires a shape with at least two anchors Revolución requiere una forma con al menos dos puntos de ancla
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} en un grupo necesita trazados, trazados compuestos, texto, grupos, grupos de recorte, fusiones e instancias de símbolo: primero libere o expanda estos elementos: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} en un grupo admite como máximo {MAX_MEMBERS} objetos
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke Un efecto 3D en un grupo o una capa se aplica al propio grupo, no a su relleno ni a su trazo
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing un efecto 3D en el relleno o el trazo de un grupo no dibuja nada
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un efecto 3D en el relleno o el trazo de un grupo no dibuja nada que exportar
|
||||
@msg The 3D object is too complex to export El objeto 3D es demasiado complejo para exportarlo
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) el objeto no tiene ningún efecto 3D (Extrusión y biselado, Revolución, Inflar o Rotar)
|
||||
@msg its 3D effect builds no solid su efecto 3D no construye ningún sólido
|
||||
@msg bevel must be none or one of {_1} bevel debe ser none o uno de estos: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} no puede construir sólidos a partir de esta ilustración: sus instancias de símbolo, fusiones y grupos de recorte contienen demasiados objetos
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} no puede construir un sólido a partir de la instancia del símbolo «{symbol}»: la ilustración del símbolo no está disponible
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: el trazado de recorte es demasiado complejo para recortar la ilustración
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} admite como máximo {MAX_MEMBERS} objetos
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} no puede construir sólidos a partir de una ilustración anidada en más de {MAX_DEPTH} niveles
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient La pintura de Revolución es demasiado compleja para esta superficie; reduzca Segmentos o simplifique el degradado
|
||||
@msg Revolve edge must be left or right Revolución: edge debe ser left o right
|
||||
@msg Revolve cap must be a boolean Revolución: cap debe ser un valor booleano
|
||||
@msg the revolved object has no fill or stroke el objeto de revolución no tiene relleno ni trazo
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor debe ser un color RGB opaco con el formato #rrggbb
|
||||
@msg lights must contain one to ten lights lights debe contener de una a diez luces
|
||||
@msg each light must be an object cada luz debe ser un objeto
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind ajuste de luz desconocido: use color, intensity, rotation, height, softness o behind
|
||||
@msg light {key} must be a number from {min} to {max} luz: {key} debe ser un número de {min} a {max}
|
||||
@msg light color must use opaque #rrggbb color de una luz debe ser un #rrggbb opaco
|
||||
@msg light behind must be true or false behind de una luz debe ser true o false
|
||||
@msg shadow must be true or false shadow debe ser true o false
|
||||
@msg shadowPosition must be behind or below shadowPosition debe ser behind o below
|
||||
@msg {key} must be a number from {min} to {max} {key} debe ser un número de {min} a {max}
|
||||
@msg each of `{GRAPHICS}` must be an object cada elemento de `{GRAPHICS}` debe ser un objeto
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` los elementos de `{GRAPHICS}` admiten symbol, face, x, y, scale y rotation, no `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol cada elemento de `{GRAPHICS}` necesita `symbol`, el nombre de un símbolo
|
||||
@msg `{GRAPHICS}` face must be front or back face de `{GRAPHICS}` debe ser front o back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} {key} de `{GRAPHICS}` debe ser un número de {lo} a {hi}
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` debe ser una matriz
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Los gráficos se asignan a caras planas: use el tipo de objeto Extrusión o Plano
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` admite como máximo {MAX_GRAPHICS} gráficos
|
||||
@msg A plane has one face: map its graphics onto the front Un plano tiene una sola cara: asigne sus gráficos a la cara frontal
|
||||
@msg The graphics are too complex: simplify the symbols' art Los gráficos son demasiado complejos: simplifique la ilustración de los símbolos
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art La ilustración de un gráfico puede anidarse como máximo en {MAX_LEVELS} niveles: simplifique la ilustración del símbolo
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art La ilustración de un gráfico puede anidar símbolos como máximo en {MAX_NESTING} niveles: simplifique la ilustración del símbolo
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art La ilustración de un gráfico contiene demasiados objetos: simplifique la ilustración del símbolo
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Un gráfico dibuja como máximo {MAX_PIECES} rellenos y trazos: simplifique la ilustración del símbolo
|
||||
@msg `surface` must be one of {_1} `surface` debe ser uno de estos: {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} dibuja la ilustración de origen de este objeto en lugar de su superficie 3D
|
||||
@msg {label} draws no faces for this object {label} no dibuja ninguna cara para este objeto
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges la estructura alámbrica tiene más de {MAX_WIRE_EDGES} aristas
|
||||
@msg The 3D surface is too complex; simplify the shape La superficie 3D es demasiado compleja; simplifique la forma
|
||||
Transform Objects Transformar objetos
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? parámetro desconocido `{key}`; ¿quiso decir `{near}`?
|
||||
@@ -6403,6 +6464,13 @@ axis Y Axis Eje Y
|
||||
@msg Batch cannot play an action with a stop Lote no puede reproducir una acción con una parada
|
||||
Insert Menu Item… Insertar elemento de menú…
|
||||
Insert Menu Item Insertar elemento de menú
|
||||
Insert Select Path Insertar selección de trazado
|
||||
Select Path Seleccionar trazado
|
||||
@msg select one whole path or compound path selecciona un trazado completo o un trazado compuesto
|
||||
@msg select only one path selecciona solo un trazado
|
||||
@msg the path is hidden or locked, or cannot be selected here el trazado está oculto o bloqueado, o no se puede seleccionar aquí
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one el objeto {value} no es el trazado guardado: ha cambiado o el documento es otro
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` debe ser "v1:" y 16 dígitos hexadecimales en minúscula
|
||||
Select Object… Seleccionar objeto…
|
||||
Select Object Seleccionar objeto
|
||||
Set Selection Definir selección
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1921,6 +1921,8 @@ digits Malayalam Malajalam
|
||||
Modified since it was read: {path} Muokattu lukemisen jälkeen: {path}
|
||||
No Thumbnails Ei pikkukuvia
|
||||
Nothing matches the Show filter. Mikään ei vastaa Näytä-suodinta.
|
||||
Show DCS Transparency Interactions Näytä DCS-läpinäkyvyyden vuorovaikutukset
|
||||
Transparent, or overlaps transparent objects: check its separations Läpinäkyvä tai läpinäkyvien objektien päällä: tarkista sen erottelut
|
||||
PPI PPI
|
||||
Relink… Linkitä uudelleen…
|
||||
Find Linked Files Etsi linkitetyt tiedostot
|
||||
@@ -5758,6 +5760,65 @@ mark-style Japanese Japanilainen
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} ei voi luoda kappaletta kuvasta: jäljitä se ensin komennolla Objekti › Kuvan jäljitys › Luo.
|
||||
@msg {label} requires a shape with at least two anchors {label} vaatii muodon, jossa on vähintään kaksi ankkuria
|
||||
@msg The shape is too complex for a 3D effect Muoto on liian monimutkainen 3D-tehosteelle
|
||||
@msg Extrude & Bevel Pursota ja viistä
|
||||
@msg Revolve Pyöräytä
|
||||
@msg Inflate Täytä ilmalla
|
||||
@msg Rotate Kierrä
|
||||
@msg Extrude & Bevel (Classic) Pursota ja viistä (perinteinen)
|
||||
@msg Revolve (Classic) Pyöräytä (perinteinen)
|
||||
@msg Rotate (Classic) Kierrä (perinteinen)
|
||||
@msg This effect Tämä tehoste
|
||||
@msg clipping paths leikkauspolut
|
||||
@msg other objects muut objektit
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} vaatii ryhmän, jonka muodossa on vähintään kaksi ankkuria
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Pyöräytä vaatii ryhmän, jonka muodossa on vähintään kaksi ankkuria
|
||||
@msg Revolve requires a shape with at least two anchors Pyöräytä vaatii muodon, jossa on vähintään kaksi ankkuria
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} ryhmässä vaatii polkuja, yhdistelmäpolkuja, tekstiä, ryhmiä, leikkausryhmiä, sulautuksia ja symbolien esiintymiä: vapauta tai laajenna ensin nämä: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} ryhmässä hyväksyy enintään {MAX_MEMBERS} objektia
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke Ryhmän tai tason 3D-tehoste kuuluu itse ryhmälle, ei sen täytölle tai viivalle
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing ryhmän täytön tai viivan 3D-tehoste ei piirrä mitään
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ryhmän täytön tai viivan 3D-tehoste ei piirrä mitään vietävää
|
||||
@msg The 3D object is too complex to export 3D-objekti on liian monimutkainen vietäväksi
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) objektilla ei ole 3D-tehostetta (Pursota ja viistä, Pyöräytä, Täytä ilmalla tai Kierrä)
|
||||
@msg its 3D effect builds no solid sen 3D-tehoste ei muodosta kappaletta
|
||||
@msg bevel must be none or one of {_1} bevel-arvon on oltava none tai jokin seuraavista: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} ei voi muodostaa kappaleita tästä kuvituksesta: sen symbolien esiintymät, sulautukset ja leikkausryhmät sisältävät liikaa objekteja
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} ei voi muodostaa kappaletta symbolin ”{symbol}” esiintymästä: symbolin kuvitus ei ole käytettävissä
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: leikkauspolku on liian monimutkainen kuvituksen leikkaamiseen
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} hyväksyy enintään {MAX_MEMBERS} objektia
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} ei voi muodostaa kappaleita kuvituksesta, joka on sisäkkäin yli {MAX_DEPTH} tason syvyydellä
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient Pyöräytä-tehosteen maalaus on liian monimutkainen tälle pinnalle; pienennä Segmentit-arvoa tai yksinkertaista liukuväriä
|
||||
@msg Revolve edge must be left or right Pyöräytä: edge-arvon on oltava left tai right
|
||||
@msg Revolve cap must be a boolean Pyöräytä: cap-arvon on oltava totuusarvo
|
||||
@msg the revolved object has no fill or stroke pyöräytetyllä objektilla ei ole täyttöä eikä viivaa
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor-arvon on oltava peittävä RGB-väri muodossa #rrggbb
|
||||
@msg lights must contain one to ten lights lights-arvossa on oltava yhdestä kymmeneen valoa
|
||||
@msg each light must be an object jokaisen valon on oltava objekti
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind tuntematon valoasetus: käytä arvoja color, intensity, rotation, height, softness tai behind
|
||||
@msg light {key} must be a number from {min} to {max} valo: {key}-arvon on oltava luku, joka on vähintään {min} ja enintään {max}
|
||||
@msg light color must use opaque #rrggbb valon color-arvon on oltava peittävä #rrggbb
|
||||
@msg light behind must be true or false valon behind-arvon on oltava true tai false
|
||||
@msg shadow must be true or false shadow-arvon on oltava true tai false
|
||||
@msg shadowPosition must be behind or below shadowPosition-arvon on oltava behind tai below
|
||||
@msg {key} must be a number from {min} to {max} {key}-arvon on oltava luku, joka on vähintään {min} ja enintään {max}
|
||||
@msg each of `{GRAPHICS}` must be an object jokaisen `{GRAPHICS}`-kohteen on oltava objekti
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` `{GRAPHICS}`-kohteet hyväksyvät arvot symbol, face, x, y, scale ja rotation, eivät arvoa `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol jokainen `{GRAPHICS}`-kohde tarvitsee arvon `symbol`, symbolin nimen
|
||||
@msg `{GRAPHICS}` face must be front or back `{GRAPHICS}`-kohteen face-arvon on oltava front tai back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} `{GRAPHICS}`-kohteen {key}-arvon on oltava luku, joka on vähintään {lo} ja enintään {hi}
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` on oltava taulukko
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Grafiikat sijoitetaan tasaisille pinnoille: käytä objektityyppiä Pursota tai Taso
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` hyväksyy enintään {MAX_GRAPHICS} grafiikkaa
|
||||
@msg A plane has one face: map its graphics onto the front Tasolla on yksi puoli: sijoita sen grafiikat etupuolelle
|
||||
@msg The graphics are too complex: simplify the symbols' art Grafiikat ovat liian monimutkaisia: yksinkertaista symbolien kuvitusta
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art Grafiikan kuvitus voi olla sisäkkäin enintään {MAX_LEVELS} tason syvyydellä: yksinkertaista symbolin kuvitusta
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art Grafiikan kuvituksessa symbolit voivat olla sisäkkäin enintään {MAX_NESTING} tason syvyydellä: yksinkertaista symbolin kuvitusta
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art Grafiikan kuvitus sisältää liikaa objekteja: yksinkertaista symbolin kuvitusta
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Grafiikka piirtää enintään {MAX_PIECES} täyttöä ja viivaa: yksinkertaista symbolin kuvitusta
|
||||
@msg `surface` must be one of {_1} `surface` on oltava jokin seuraavista: {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} piirtää tälle objektille lähdekuvituksen sen 3D-pinnan sijaan
|
||||
@msg {label} draws no faces for this object {label} ei piirrä tälle objektille yhtään pintaa
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges rautalankamallissa on yli {MAX_WIRE_EDGES} reunaa
|
||||
@msg The 3D surface is too complex; simplify the shape 3D-pinta on liian monimutkainen; yksinkertaista muotoa
|
||||
Transform Objects Muunna objekteja
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? tuntematon parametri `{key}`; tarkoititko `{near}`?
|
||||
@@ -6491,6 +6552,13 @@ axis Y Axis Y-akseli
|
||||
@msg Batch cannot play an action with a stop Erä ei voi toistaa toimintoa, jossa on pysäytys
|
||||
Insert Menu Item… Lisää valikkokohta…
|
||||
Insert Menu Item Lisää valikkokohta
|
||||
Insert Select Path Lisää polun valinta
|
||||
Select Path Valitse polku
|
||||
@msg select one whole path or compound path valitse yksi kokonainen polku tai yhdistelmäpolku
|
||||
@msg select only one path valitse vain yksi polku
|
||||
@msg the path is hidden or locked, or cannot be selected here polku on piilotettu tai lukittu, tai sitä ei voi valita tässä
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one objekti {value} ei ole tallennettu polku: se on muuttunut, tai asiakirja on toinen
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` on oltava "v1:" ja 16 pientä heksadesimaalinumeroa
|
||||
Select Object… Valitse objekti…
|
||||
Select Object Valitse objekti
|
||||
Set Selection Määritä valinta
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2268,6 +2268,8 @@ font-technology Variable Variables
|
||||
Normal Screen Mode Mode Écran normal
|
||||
Note: Remarque :
|
||||
Nothing matches the Show filter. Aucun élément ne correspond au filtre Afficher.
|
||||
Show DCS Transparency Interactions Afficher les interactions de transparence DCS
|
||||
Transparent, or overlaps transparent objects: check its separations Transparent ou superposé à des objets transparents : vérifiez ses séparations
|
||||
Nothing to print Rien à imprimer
|
||||
Nudge Décaler
|
||||
Nudge in Perspective Décaler en perspective
|
||||
@@ -5446,6 +5448,65 @@ lighting Light Lumière
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} ne peut pas créer de solide à partir d'une image : vectorisez-la d'abord avec Objet › Vectorisation de l’image › Créer.
|
||||
@msg {label} requires a shape with at least two anchors {label} nécessite une forme comportant au moins deux points d'ancrage
|
||||
@msg The shape is too complex for a 3D effect La forme est trop complexe pour un effet 3D
|
||||
@msg Extrude & Bevel Extrusion et biseautage
|
||||
@msg Revolve Révolution
|
||||
@msg Inflate Gonfler
|
||||
@msg Rotate Rotation
|
||||
@msg Extrude & Bevel (Classic) Extrusion et biseautage (classique)
|
||||
@msg Revolve (Classic) Révolution (classique)
|
||||
@msg Rotate (Classic) Rotation (classique)
|
||||
@msg This effect Cet effet
|
||||
@msg clipping paths tracés de découpe
|
||||
@msg other objects autres objets
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} nécessite un groupe contenant une forme comportant au moins deux points d'ancrage
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Révolution nécessite un groupe contenant une forme comportant au moins deux points d'ancrage
|
||||
@msg Revolve requires a shape with at least two anchors Révolution nécessite une forme comportant au moins deux points d'ancrage
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} sur un groupe nécessite des tracés, tracés transparents, textes, groupes, groupes de découpe, dégradés de formes et occurrences de symbole : libérez ou développez d'abord ces éléments : {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} sur un groupe accepte au plus {MAX_MEMBERS} objets
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke Un effet 3D sur un groupe ou un calque s'applique au groupe lui-même, pas à son fond ni à son contour
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing un effet 3D sur le fond ou le contour d'un groupe ne dessine rien
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un effet 3D sur le fond ou le contour d'un groupe ne dessine rien à exporter
|
||||
@msg The 3D object is too complex to export L'objet 3D est trop complexe pour être exporté
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) l'objet n'a aucun effet 3D (Extrusion et biseautage, Révolution, Gonfler ou Rotation)
|
||||
@msg its 3D effect builds no solid son effet 3D ne construit aucun volume
|
||||
@msg bevel must be none or one of {_1} bevel doit valoir none ou l’une de ces valeurs : {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} ne peut pas construire de volumes à partir de cette illustration : ses occurrences de symbole, dégradés de formes et groupes de découpe contiennent trop d'objets
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} ne peut pas construire de volume à partir de l'occurrence du symbole « {symbol} » : l'illustration du symbole n'est pas disponible
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1} : le tracé de découpe est trop complexe pour découper l'illustration
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} accepte au plus {MAX_MEMBERS} objets
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} ne peut pas construire de volumes à partir d'une illustration imbriquée sur plus de {MAX_DEPTH} niveaux
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient La peinture de Révolution est trop complexe pour cette surface ; réduisez Segments ou simplifiez le dégradé
|
||||
@msg Revolve edge must be left or right Révolution : edge doit valoir left ou right
|
||||
@msg Revolve cap must be a boolean Révolution : cap doit être un booléen
|
||||
@msg the revolved object has no fill or stroke l'objet en révolution n'a ni fond ni contour
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor doit être une couleur RVB opaque au format #rrggbb
|
||||
@msg lights must contain one to ten lights lights doit contenir de une à dix lumières
|
||||
@msg each light must be an object chaque lumière doit être un objet
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind réglage de lumière inconnu : utilisez color, intensity, rotation, height, softness ou behind
|
||||
@msg light {key} must be a number from {min} to {max} lumière : {key} doit être un nombre de {min} à {max}
|
||||
@msg light color must use opaque #rrggbb color d'une lumière doit être une couleur #rrggbb opaque
|
||||
@msg light behind must be true or false behind d'une lumière doit valoir true ou false
|
||||
@msg shadow must be true or false shadow doit valoir true ou false
|
||||
@msg shadowPosition must be behind or below shadowPosition doit valoir behind ou below
|
||||
@msg {key} must be a number from {min} to {max} {key} doit être un nombre de {min} à {max}
|
||||
@msg each of `{GRAPHICS}` must be an object chaque élément de `{GRAPHICS}` doit être un objet
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` les éléments de `{GRAPHICS}` acceptent symbol, face, x, y, scale et rotation, pas `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol chaque élément de `{GRAPHICS}` nécessite `symbol`, le nom d'un symbole
|
||||
@msg `{GRAPHICS}` face must be front or back face de `{GRAPHICS}` doit valoir front ou back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} {key} de `{GRAPHICS}` doit être un nombre de {lo} à {hi}
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` doit être un tableau
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Les graphiques s'appliquent sur des faces planes : utilisez le type d'objet Extrusion ou Plan
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` accepte au plus {MAX_GRAPHICS} graphiques
|
||||
@msg A plane has one face: map its graphics onto the front Un plan n'a qu'une face : appliquez ses graphiques sur la face avant
|
||||
@msg The graphics are too complex: simplify the symbols' art Les graphiques sont trop complexes : simplifiez l'illustration des symboles
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art L'illustration d'un graphique peut être imbriquée sur {MAX_LEVELS} niveaux au plus : simplifiez l'illustration du symbole
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art L'illustration d'un graphique peut imbriquer des symboles sur {MAX_NESTING} niveaux au plus : simplifiez l'illustration du symbole
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art L'illustration d'un graphique contient trop d'objets : simplifiez l'illustration du symbole
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Un graphique dessine au plus {MAX_PIECES} fonds et contours : simplifiez l'illustration du symbole
|
||||
@msg `surface` must be one of {_1} `surface` doit valoir l’une de ces valeurs : {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} dessine l'illustration source de cet objet au lieu de sa surface 3D
|
||||
@msg {label} draws no faces for this object {label} ne dessine aucune face pour cet objet
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges le fil de fer comporte plus de {MAX_WIRE_EDGES} arêtes
|
||||
@msg The 3D surface is too complex; simplify the shape La surface 3D est trop complexe ; simplifiez la forme
|
||||
Transform Objects Transformer les objets
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? paramètre inconnu `{key}` ; vouliez-vous dire `{near}` ?
|
||||
@@ -6456,6 +6517,13 @@ axis Y Axis Axe Y
|
||||
@msg Batch cannot play an action with a stop Le traitement par lots ne peut pas exécuter un script comportant un arrêt
|
||||
Insert Menu Item… Insérer une commande de menu…
|
||||
Insert Menu Item Insérer une commande de menu
|
||||
Insert Select Path Insérer une sélection de tracé
|
||||
Select Path Sélectionner le tracé
|
||||
@msg select one whole path or compound path sélectionnez un tracé entier ou un tracé transparent
|
||||
@msg select only one path sélectionnez un seul tracé
|
||||
@msg the path is hidden or locked, or cannot be selected here le tracé est masqué ou verrouillé, ou ne peut pas être sélectionné ici
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one l’objet {value} n’est pas le tracé enregistré : il a été modifié, ou il s’agit d’un autre document
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` doit être "v1:" suivi de 16 chiffres hexadécimaux en minuscules
|
||||
Select Object… Sélectionner un objet…
|
||||
Select Object Sélectionner un objet
|
||||
Set Selection Définir la sélection
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3065,6 +3065,8 @@ antiAlias Sharp शार्प
|
||||
Note: ध्यान दें:
|
||||
Nothing is selected. Select the art to export, then open this tab again. कुछ भी नहीं चुना गया है। निर्यात करने के लिए कला सिलेक्ट करें, फिर इस टैब को फिर से ओपन करें।
|
||||
Nothing matches the Show filter. कुछ भी नहीं शो फिल्टर से मेल खाता है।
|
||||
Show DCS Transparency Interactions DCS पारदर्शिता इंटरैक्शन दिखाएँ
|
||||
Transparent, or overlaps transparent objects: check its separations पारदर्शी है या पारदर्शी ऑब्जेक्ट पर चढ़ता है: इसके सेपरेशन जाँचें
|
||||
Nothing to print प्रिंट करने के लिए कुछ भी नहीं
|
||||
NTSC Colors NTSC रंग
|
||||
Nudge न्याय
|
||||
@@ -3470,6 +3472,13 @@ antiAlias Sharp शार्प
|
||||
@msg Batch cannot play an action with a stop बैच स्टॉप वाला एक्शन नहीं चला सकता
|
||||
Insert Menu Item… मेनू आइटम जोड़ें…
|
||||
Insert Menu Item मेनू आइटम जोड़ें
|
||||
Insert Select Path सिलेक्ट पाथ जोड़ें
|
||||
Select Path पाथ सिलेक्ट करें
|
||||
@msg select one whole path or compound path एक पूरा पाथ या कंपाउंड पाथ चुनें
|
||||
@msg select only one path केवल एक पाथ चुनें
|
||||
@msg the path is hidden or locked, or cannot be selected here पाथ छिपा हुआ है या लॉक है, या यहाँ चुना नहीं जा सकता
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one ऑब्जेक्ट {value} सहेजा गया पाथ नहीं है: वह बदल गया है, या डॉक्यूमेंट दूसरा है
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` "v1:" और छोटे अक्षरों वाले 16 हेक्स अंक होना चाहिए
|
||||
Select Object… ऑब्जेक्ट सिलेक्ट करें…
|
||||
Select Object ऑब्जेक्ट सिलेक्ट करें
|
||||
Set Selection सिलेक्शन सेट करें
|
||||
@@ -7853,6 +7862,65 @@ antiAlias Sharp शार्प
|
||||
@msg that makes too many pieces (at most 100000): make the dashes longer जो बहुत सारे टुकड़े बनाता है (अधिकांश 100000 में): डैश को लंबे समय तक बनाते हैं
|
||||
@msg that object is not in the overlap उस ऑब्जेक्ट को ओवरलैप में नहीं है
|
||||
@msg the {tool} tool has no options {tool} टूल में कोई ऑप्शन्स नहीं है
|
||||
@msg Extrude & Bevel एक्सट्रूडेड और बेवेल
|
||||
@msg Revolve रिवॉल्व
|
||||
@msg Inflate फुलाएँ
|
||||
@msg Rotate घुमाव
|
||||
@msg Extrude & Bevel (Classic) एक्सट्रूडेड और बेवेल (क्लासिक)
|
||||
@msg Revolve (Classic) रिवॉल्व (क्लासिक)
|
||||
@msg Rotate (Classic) घुमाव (क्लासिक)
|
||||
@msg This effect यह इफ़ेक्ट
|
||||
@msg clipping paths क्लिपिंग पाथ
|
||||
@msg other objects अन्य ऑब्जेक्ट
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} को कम से कम दो एंकरों वाले आकार से युक्त ग्रुप की आवश्यकता होती है
|
||||
@msg Revolve requires a group holding a shape with at least two anchors रिवॉल्व को कम से कम दो एंकरों वाले आकार से युक्त ग्रुप की आवश्यकता होती है
|
||||
@msg Revolve requires a shape with at least two anchors रिवॉल्व को कम से कम दो एंकरों वाले आकार की आवश्यकता होती है
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. ग्रुप पर {label} के लिए पाथ, कंपाउंड पाथ, टाइप, ग्रुप, क्लिपिंग ग्रुप, ब्लेंड और सिंबल इंस्टेंस चाहिए: पहले इन्हें रिलीज़ या एक्सपैंड करें: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects ग्रुप पर {label} अधिकतम {MAX_MEMBERS} ऑब्जेक्ट लेता है
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke किसी ग्रुप या लेयर पर 3D इफ़ेक्ट ग्रुप पर ही लगता है, उसके फ़िल या स्ट्रोक पर नहीं
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing ग्रुप के फ़िल या स्ट्रोक पर 3D इफ़ेक्ट कुछ नहीं बनाता
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ग्रुप के फ़िल या स्ट्रोक पर 3D इफ़ेक्ट एक्सपोर्ट करने लायक कुछ नहीं बनाता
|
||||
@msg The 3D object is too complex to export 3D ऑब्जेक्ट एक्सपोर्ट करने के लिए बहुत जटिल है
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) ऑब्जेक्ट पर कोई 3D इफ़ेक्ट नहीं है (एक्सट्रूडेड और बेवेल, रिवॉल्व, फुलाएँ या घुमाव)
|
||||
@msg its 3D effect builds no solid इसका 3D इफ़ेक्ट कोई ठोस नहीं बनाता
|
||||
@msg bevel must be none or one of {_1} bevel को none या {_1} में से एक होना चाहिए
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} इस आर्ट से ठोस नहीं बना सकता: इसके सिंबल इंस्टेंस, ब्लेंड और क्लिपिंग ग्रुप में बहुत अधिक ऑब्जेक्ट हैं
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} सिंबल "{symbol}" के इंस्टेंस से ठोस नहीं बना सकता: सिंबल का आर्ट उपलब्ध नहीं है
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: क्लिपिंग पाथ आर्ट को काटने के लिए बहुत जटिल है
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} अधिकतम {MAX_MEMBERS} ऑब्जेक्ट लेता है
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} {MAX_DEPTH} स्तर से अधिक गहराई तक नेस्ट किए गए आर्ट से ठोस नहीं बना सकता
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient रिवॉल्व का पेंट इस सतह के लिए बहुत जटिल है; सेगमेंट कम करें या ग्रेडिएंट को सरल बनाएँ
|
||||
@msg Revolve edge must be left or right रिवॉल्व: edge को left या right होना चाहिए
|
||||
@msg Revolve cap must be a boolean रिवॉल्व: cap एक बूलियन होना चाहिए
|
||||
@msg the revolved object has no fill or stroke रिवॉल्व किए गए ऑब्जेक्ट में कोई फ़िल या स्ट्रोक नहीं है
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor को #rrggbb रूप में अपारदर्शी RGB रंग होना चाहिए
|
||||
@msg lights must contain one to ten lights lights में एक से दस लाइटें होनी चाहिए
|
||||
@msg each light must be an object हर लाइट एक ऑब्जेक्ट होनी चाहिए
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind अज्ञात लाइट सेटिंग: color, intensity, rotation, height, softness या behind का उपयोग करें
|
||||
@msg light {key} must be a number from {min} to {max} लाइट: {key} को {min} से {max} तक की संख्या होना चाहिए
|
||||
@msg light color must use opaque #rrggbb लाइट का color अपारदर्शी #rrggbb होना चाहिए
|
||||
@msg light behind must be true or false लाइट का behind, true या false होना चाहिए
|
||||
@msg shadow must be true or false shadow को true या false होना चाहिए
|
||||
@msg shadowPosition must be behind or below shadowPosition को behind या below होना चाहिए
|
||||
@msg {key} must be a number from {min} to {max} {key} को {min} से {max} तक की संख्या होना चाहिए
|
||||
@msg each of `{GRAPHICS}` must be an object `{GRAPHICS}` की हर प्रविष्टि एक ऑब्जेक्ट होनी चाहिए
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` `{GRAPHICS}` की प्रविष्टियाँ symbol, face, x, y, scale और rotation लेती हैं, `{k}` नहीं
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol `{GRAPHICS}` की हर प्रविष्टि को `symbol` चाहिए, यानी किसी सिंबल का नाम
|
||||
@msg `{GRAPHICS}` face must be front or back `{GRAPHICS}` का face, front या back होना चाहिए
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} `{GRAPHICS}` का {key}, {lo} से {hi} तक की संख्या होना चाहिए
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` एक ऐरे होना चाहिए
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type ग्राफ़िक्स समतल फ़ेस पर लगते हैं: एक्सट्रूड या समतल ऑब्जेक्ट प्रकार का उपयोग करें
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` अधिकतम {MAX_GRAPHICS} ग्राफ़िक्स लेता है
|
||||
@msg A plane has one face: map its graphics onto the front समतल का एक ही फ़ेस होता है: उसके ग्राफ़िक्स को सामने वाले फ़ेस पर लगाएँ
|
||||
@msg The graphics are too complex: simplify the symbols' art ग्राफ़िक्स बहुत जटिल हैं: सिंबल्स के आर्ट को सरल बनाएँ
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art ग्राफ़िक का आर्ट अधिकतम {MAX_LEVELS} स्तर तक नेस्ट हो सकता है: सिंबल के आर्ट को सरल बनाएँ
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art ग्राफ़िक का आर्ट सिंबल को अधिकतम {MAX_NESTING} स्तर तक नेस्ट कर सकता है: सिंबल के आर्ट को सरल बनाएँ
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art ग्राफ़िक के आर्ट में बहुत अधिक ऑब्जेक्ट हैं: सिंबल के आर्ट को सरल बनाएँ
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art एक ग्राफ़िक अधिकतम {MAX_PIECES} फ़िल और स्ट्रोक बनाता है: सिंबल के आर्ट को सरल बनाएँ
|
||||
@msg `surface` must be one of {_1} `surface` को {_1} में से एक होना चाहिए
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} इस ऑब्जेक्ट के लिए उसकी 3D सतह के बजाय स्रोत आर्ट बनाता है
|
||||
@msg {label} draws no faces for this object {label} इस ऑब्जेक्ट के लिए कोई फ़ेस नहीं बनाता
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges वायरफ़्रेम में {MAX_WIRE_EDGES} से अधिक किनारे हैं
|
||||
@msg The 3D surface is too complex; simplify the shape 3D सतह बहुत जटिल है; आकार को सरल बनाएं
|
||||
@msg the action has no included steps कार्रवाई में कोई कदम शामिल नहीं है
|
||||
@msg the action has no steps कार्रवाई में कोई कदम नहीं है
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2753,6 +2753,8 @@ file-filter Templates Sablonok
|
||||
Not found Nem található
|
||||
Note: Megjegyzés:
|
||||
Nothing matches the Show filter. A Megjelenítés szűrőnek semmi sem felel meg.
|
||||
Show DCS Transparency Interactions DCS-átlátszósági kölcsönhatások megjelenítése
|
||||
Transparent, or overlaps transparent objects: check its separations Átlátszó, vagy átlátszó objektumokat fed át: ellenőrizze a színkivonatait
|
||||
Nothing to print Nincs mit nyomtatni
|
||||
NTSC Colors NTSC-színek
|
||||
Nudge Léptetés
|
||||
@@ -5859,6 +5861,65 @@ imageTrace Outline Körvonal
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} képből nem tud testet létrehozni: előbb vektorizálja az Objektum › Képvektorizálás › Létrehozás paranccsal.
|
||||
@msg {label} requires a shape with at least two anchors {label} legalább két szerkesztőpontos alakzatot igényel
|
||||
@msg The shape is too complex for a 3D effect Az alakzat túl összetett a 3D effektushoz
|
||||
@msg Extrude & Bevel Kihúzás és élletörés
|
||||
@msg Revolve Forgatás tengely körül
|
||||
@msg Inflate Felfújás
|
||||
@msg Rotate Elforgatás
|
||||
@msg Extrude & Bevel (Classic) Kihúzás és élletörés (klasszikus)
|
||||
@msg Revolve (Classic) Forgatás tengely körül (klasszikus)
|
||||
@msg Rotate (Classic) Elforgatás (klasszikus)
|
||||
@msg This effect Ez az effektus
|
||||
@msg clipping paths vágóútvonalak
|
||||
@msg other objects egyéb objektumok
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label}: legalább két szerkesztőpontos alakzatot tartalmazó csoport szükséges
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Forgatás tengely körül: legalább két szerkesztőpontos alakzatot tartalmazó csoport szükséges
|
||||
@msg Revolve requires a shape with at least two anchors Forgatás tengely körül: legalább két szerkesztőpontos alakzat szükséges
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} csoporton: útvonalak, összetett útvonalak, szöveg, csoportok, vágócsoportok, összemosások és szimbólumpéldányok szükségesek; előbb bontsa fel vagy alakítsa át ezeket: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} csoporton: legfeljebb {MAX_MEMBERS} objektum lehet
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke A csoporton vagy rétegen lévő 3D effektus magára a csoportra kerül, nem a kitöltésére vagy körvonalára
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing a csoport kitöltésén vagy körvonalán lévő 3D effektus nem rajzol semmit
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export a csoport kitöltésén vagy körvonalán lévő 3D effektus nem rajzol semmi exportálhatót
|
||||
@msg The 3D object is too complex to export A 3D objektum túl összetett az exportáláshoz
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) az objektumnak nincs 3D effektusa (Kihúzás és élletörés, Forgatás tengely körül, Felfújás vagy Elforgatás)
|
||||
@msg its 3D effect builds no solid a 3D effektusa nem hoz létre testet
|
||||
@msg bevel must be none or one of {_1} a bevel értéke none vagy ezek egyike lehet: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: ebből a rajzból nem hozható létre test: a szimbólumpéldányai, összemosásai és vágócsoportjai túl sok objektumot tartalmaznak
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label}: nem hozható létre test a(z) „{symbol}” szimbólum példányából: a szimbólum rajza nem érhető el
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: a vágóútvonal túl összetett a rajz kivágásához
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1}: legfeljebb {MAX_MEMBERS} objektum lehet
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1}: nem hozható létre test {MAX_DEPTH} szintnél mélyebben egymásba ágyazott rajzból
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient A Forgatás tengely körül festése túl összetett ehhez a felülethez; csökkentse a Szakaszok értékét, vagy egyszerűsítse a színátmenetet
|
||||
@msg Revolve edge must be left or right Forgatás tengely körül: az edge értéke left vagy right lehet
|
||||
@msg Revolve cap must be a boolean Forgatás tengely körül: a cap értéke logikai érték lehet
|
||||
@msg the revolved object has no fill or stroke a forgatott objektumnak nincs kitöltése vagy körvonala
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form a lightColor értéke #rrggbb formájú átlátszatlan RGB szín lehet
|
||||
@msg lights must contain one to ten lights a lights egy és tíz közötti számú fényt tartalmazhat
|
||||
@msg each light must be an object minden fénynek objektumnak kell lennie
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind ismeretlen fénybeállítás: ezeket használja: color, intensity, rotation, height, softness vagy behind
|
||||
@msg light {key} must be a number from {min} to {max} fény: a {key} értéke {min} és {max} közötti szám lehet
|
||||
@msg light color must use opaque #rrggbb egy fény color értéke átlátszatlan #rrggbb lehet
|
||||
@msg light behind must be true or false egy fény behind értéke true vagy false lehet
|
||||
@msg shadow must be true or false a shadow értéke true vagy false lehet
|
||||
@msg shadowPosition must be behind or below a shadowPosition értéke behind vagy below lehet
|
||||
@msg {key} must be a number from {min} to {max} a {key} értéke {min} és {max} közötti szám lehet
|
||||
@msg each of `{GRAPHICS}` must be an object a `{GRAPHICS}` minden elemének objektumnak kell lennie
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` a `{GRAPHICS}` elemei symbol, face, x, y, scale és rotation értéket fogadnak, nem `{k}` értéket
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol a `{GRAPHICS}` minden eleméhez kell `symbol`, egy szimbólum neve
|
||||
@msg `{GRAPHICS}` face must be front or back a `{GRAPHICS}` face értéke front vagy back lehet
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} a `{GRAPHICS}` {key} értéke {lo} és {hi} közötti szám lehet
|
||||
@msg `{GRAPHICS}` must be an array a `{GRAPHICS}` értéke tömb kell legyen
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type A grafikák sík lapokra kerülnek: használja a Kihúzás vagy a Sík objektumtípust
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics a `{GRAPHICS}` legfeljebb {MAX_GRAPHICS} grafikát fogad
|
||||
@msg A plane has one face: map its graphics onto the front A síknak egy oldala van: a grafikáit az elülső oldalra helyezze
|
||||
@msg The graphics are too complex: simplify the symbols' art A grafikák túl összetettek: egyszerűsítse a szimbólumok rajzát
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art Egy grafika rajza legfeljebb {MAX_LEVELS} szint mélyen lehet egymásba ágyazva: egyszerűsítse a szimbólum rajzát
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art Egy grafika rajza legfeljebb {MAX_NESTING} szint mélyen ágyazhat egymásba szimbólumokat: egyszerűsítse a szimbólum rajzát
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art Egy grafika rajza túl sok objektumot tartalmaz: egyszerűsítse a szimbólum rajzát
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Egy grafika legfeljebb {MAX_PIECES} kitöltést és körvonalat rajzol: egyszerűsítse a szimbólum rajzát
|
||||
@msg `surface` must be one of {_1} `surface`: ezek egyike kell: {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label}: ehhez az objektumhoz a forrásrajzot rajzolja a 3D felülete helyett
|
||||
@msg {label} draws no faces for this object {label}: ehhez az objektumhoz nem rajzol lapot
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges a drótváznak több mint {MAX_WIRE_EDGES} éle van
|
||||
@msg The 3D surface is too complex; simplify the shape A 3D felület túl összetett; egyszerűsítse az alakzatot
|
||||
Transform Objects Objektumok transzformálása
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? ismeretlen paraméter: `{key}`; erre gondolt: `{near}`?
|
||||
@@ -6444,6 +6505,13 @@ axis Y Axis Y tengely
|
||||
@msg Batch cannot play an action with a stop A kötegelt feldolgozás nem játszhat le megállást tartalmazó műveletet
|
||||
Insert Menu Item… Menüelem beszúrása…
|
||||
Insert Menu Item Menüelem beszúrása
|
||||
Insert Select Path Útvonal kijelölésének beszúrása
|
||||
Select Path Útvonal kijelölése
|
||||
@msg select one whole path or compound path jelölj ki egy teljes útvonalat vagy összetett útvonalat
|
||||
@msg select only one path csak egy útvonalat jelölj ki
|
||||
@msg the path is hidden or locked, or cannot be selected here az útvonal rejtett vagy zárolt, vagy itt nem jelölhető ki
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one a(z) {value} objektum nem a tárolt útvonal: megváltozott, vagy ez egy másik dokumentum
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits a `fingerprint` értéke "v1:" és 16 kisbetűs hexadecimális számjegy kell legyen
|
||||
Select Object… Objektum kijelölése…
|
||||
Select Object Objektum kijelölése
|
||||
Set Selection Kijelölés beállítása
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1775,6 +1775,8 @@ digits Malayalam Malayalam
|
||||
Modified since it was read: {path} Diubah sejak terakhir dibaca: {path}
|
||||
No Thumbnails Tanpa thumbnail
|
||||
Nothing matches the Show filter. Tidak ada yang cocok dengan filter Tampilkan.
|
||||
Show DCS Transparency Interactions Tampilkan Interaksi Transparansi DCS
|
||||
Transparent, or overlaps transparent objects: check its separations Transparan, atau menimpa objek transparan: periksa separasinya
|
||||
PPI PPI
|
||||
Relink… Tautkan ulang…
|
||||
Find Linked Files Temukan file tertaut
|
||||
@@ -6004,6 +6006,13 @@ axis Y Axis Sumbu Y
|
||||
@msg Batch cannot play an action with a stop Batch tidak dapat memutar tindakan dengan titik henti
|
||||
Insert Menu Item… Sisipkan item menu…
|
||||
Insert Menu Item Sisipkan item menu
|
||||
Insert Select Path Sisipkan pemilihan jalur
|
||||
Select Path Pilih jalur
|
||||
@msg select one whole path or compound path pilih satu jalur utuh atau jalur majemuk
|
||||
@msg select only one path pilih hanya satu jalur
|
||||
@msg the path is hidden or locked, or cannot be selected here jalur tersembunyi atau terkunci, atau tidak dapat dipilih di sini
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one objek {value} bukan jalur yang disimpan: jalur itu berubah, atau dokumennya berbeda
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` harus berupa "v1:" dan 16 digit heksadesimal huruf kecil
|
||||
Select Object… Pilih objek…
|
||||
Select Object Pilih objek
|
||||
Set Selection Atur seleksi
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2412 and column 5.
|
@@ -2314,6 +2314,8 @@ file-filter Templates Modelli
|
||||
Normal Screen Mode Modalità schermo normale
|
||||
Note: Nota:
|
||||
Nothing matches the Show filter. Nessun elemento corrisponde al filtro Mostra.
|
||||
Show DCS Transparency Interactions Mostra interazioni di trasparenza DCS
|
||||
Transparent, or overlaps transparent objects: check its separations Trasparente o sovrapposto a oggetti trasparenti: controlla le sue separazioni
|
||||
Nothing to print Niente da stampare
|
||||
Nudge Sposta leggermente
|
||||
Nudge in Perspective Sposta leggermente in prospettiva
|
||||
@@ -5493,6 +5495,65 @@ lighting Light Luce
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} non può creare un solido da un'immagine: ricalcatela prima con Oggetto › Ricalco immagine › Crea.
|
||||
@msg {label} requires a shape with at least two anchors {label} richiede una forma con almeno due punti di ancoraggio
|
||||
@msg The shape is too complex for a 3D effect La forma è troppo complessa per un effetto 3D
|
||||
@msg Extrude & Bevel Estrusione e smusso
|
||||
@msg Revolve Rivoluzione
|
||||
@msg Inflate Gonfia
|
||||
@msg Rotate Ruota
|
||||
@msg Extrude & Bevel (Classic) Estrusione e smusso (classico)
|
||||
@msg Revolve (Classic) Rivoluzione (classico)
|
||||
@msg Rotate (Classic) Ruota (classico)
|
||||
@msg This effect Questo effetto
|
||||
@msg clipping paths tracciati di ritaglio
|
||||
@msg other objects altri oggetti
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} richiede un gruppo che contenga una forma con almeno due punti di ancoraggio
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Rivoluzione richiede un gruppo che contenga una forma con almeno due punti di ancoraggio
|
||||
@msg Revolve requires a shape with at least two anchors Rivoluzione richiede una forma con almeno due punti di ancoraggio
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} su un gruppo richiede tracciati, tracciati composti, testo, gruppi, gruppi di ritaglio, fusioni e istanze di simbolo: prima rilasciate o espandete questi elementi: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} su un gruppo accetta al massimo {MAX_MEMBERS} oggetti
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke Un effetto 3D su un gruppo o un livello va applicato al gruppo stesso, non al suo riempimento o alla sua traccia
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing un effetto 3D sul riempimento o sulla traccia di un gruppo non disegna nulla
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un effetto 3D sul riempimento o sulla traccia di un gruppo non disegna nulla da esportare
|
||||
@msg The 3D object is too complex to export L'oggetto 3D è troppo complesso per essere esportato
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) l'oggetto non ha alcun effetto 3D (Estrusione e smusso, Rivoluzione, Gonfia o Ruota)
|
||||
@msg its 3D effect builds no solid il suo effetto 3D non costruisce alcun solido
|
||||
@msg bevel must be none or one of {_1} bevel deve essere none o uno dei seguenti: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} non può costruire solidi da questo disegno: le sue istanze di simbolo, fusioni e gruppi di ritaglio contengono troppi oggetti
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} non può costruire un solido dall'istanza del simbolo «{symbol}»: il disegno del simbolo non è disponibile
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: il tracciato di ritaglio è troppo complesso per ritagliare il disegno
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} accetta al massimo {MAX_MEMBERS} oggetti
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} non può costruire solidi da un disegno nidificato per più di {MAX_DEPTH} livelli
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient La pittura di Rivoluzione è troppo complessa per questa superficie; riducete Segmenti o semplificate la sfumatura
|
||||
@msg Revolve edge must be left or right Rivoluzione: edge deve essere left o right
|
||||
@msg Revolve cap must be a boolean Rivoluzione: cap deve essere un valore booleano
|
||||
@msg the revolved object has no fill or stroke l'oggetto in rivoluzione non ha riempimento né traccia
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor deve essere un colore RGB opaco nel formato #rrggbb
|
||||
@msg lights must contain one to ten lights lights deve contenere da una a dieci luci
|
||||
@msg each light must be an object ogni luce deve essere un oggetto
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind impostazione della luce sconosciuta: usate color, intensity, rotation, height, softness o behind
|
||||
@msg light {key} must be a number from {min} to {max} luce: {key} deve essere un numero da {min} a {max}
|
||||
@msg light color must use opaque #rrggbb color di una luce deve essere un #rrggbb opaco
|
||||
@msg light behind must be true or false behind di una luce deve essere true o false
|
||||
@msg shadow must be true or false shadow deve essere true o false
|
||||
@msg shadowPosition must be behind or below shadowPosition deve essere behind o below
|
||||
@msg {key} must be a number from {min} to {max} {key} deve essere un numero da {min} a {max}
|
||||
@msg each of `{GRAPHICS}` must be an object ogni voce di `{GRAPHICS}` deve essere un oggetto
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` le voci di `{GRAPHICS}` accettano symbol, face, x, y, scale e rotation, non `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol ogni voce di `{GRAPHICS}` richiede `symbol`, il nome di un simbolo
|
||||
@msg `{GRAPHICS}` face must be front or back face di `{GRAPHICS}` deve essere front o back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} {key} di `{GRAPHICS}` deve essere un numero da {lo} a {hi}
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` deve essere un array
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Le grafiche si applicano a facce piane: usate il tipo di oggetto Estrusione o Piano
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` accetta al massimo {MAX_GRAPHICS} grafiche
|
||||
@msg A plane has one face: map its graphics onto the front Un piano ha una sola faccia: applicate le sue grafiche sulla faccia anteriore
|
||||
@msg The graphics are too complex: simplify the symbols' art Le grafiche sono troppo complesse: semplificate il disegno dei simboli
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art Il disegno di una grafica può essere nidificato al massimo per {MAX_LEVELS} livelli: semplificate il disegno del simbolo
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art Il disegno di una grafica può nidificare simboli al massimo per {MAX_NESTING} livelli: semplificate il disegno del simbolo
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art Il disegno di una grafica contiene troppi oggetti: semplificate il disegno del simbolo
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Una grafica disegna al massimo {MAX_PIECES} riempimenti e tracce: semplificate il disegno del simbolo
|
||||
@msg `surface` must be one of {_1} `surface` deve essere uno dei seguenti: {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} mostra il disegno di origine di questo oggetto al posto della sua superficie 3D
|
||||
@msg {label} draws no faces for this object {label} non disegna alcuna faccia per questo oggetto
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges il wireframe ha più di {MAX_WIRE_EDGES} spigoli
|
||||
@msg The 3D surface is too complex; simplify the shape La superficie 3D è troppo complessa; semplificate la forma
|
||||
Transform Objects Trasforma oggetti
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? parametro sconosciuto `{key}`; forse intendevate `{near}`?
|
||||
@@ -6086,6 +6147,13 @@ axis Y Axis Asse Y
|
||||
@msg Batch cannot play an action with a stop L'elaborazione batch non può eseguire un'azione con un'interruzione
|
||||
Insert Menu Item… Inserisci voce di menu…
|
||||
Insert Menu Item Inserisci voce di menu
|
||||
Insert Select Path Inserisci selezione tracciato
|
||||
Select Path Seleziona tracciato
|
||||
@msg select one whole path or compound path seleziona un tracciato intero o un tracciato composto
|
||||
@msg select only one path seleziona un solo tracciato
|
||||
@msg the path is hidden or locked, or cannot be selected here il tracciato è nascosto o bloccato, oppure non può essere selezionato qui
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one l’oggetto {value} non è il tracciato memorizzato: è cambiato, oppure il documento è un altro
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` deve essere "v1:" seguito da 16 cifre esadecimali minuscole
|
||||
Select Object… Seleziona oggetto…
|
||||
Select Object Seleziona oggetto
|
||||
Set Selection Imposta selezione
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2493,6 +2493,8 @@ antiAlias Sharp シャープ
|
||||
Normal 通常
|
||||
Note: メモ:
|
||||
Nothing matches the Show filter. 表示フィルターに一致する項目がありません。
|
||||
Show DCS Transparency Interactions DCS の透明の相互作用を表示
|
||||
Transparent, or overlaps transparent objects: check its separations 透明、または透明なオブジェクトと重なっています:分版を確認してください
|
||||
Nothing to print プリントするものがありません
|
||||
Nudge 微調整
|
||||
Nudge in Perspective 遠近で微調整
|
||||
@@ -5535,6 +5537,65 @@ lighting Light ライト
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} は画像から立体を作成できません。先に「オブジェクト › 画像トレース › 作成」でトレースしてください。
|
||||
@msg {label} requires a shape with at least two anchors {label} には 2 つ以上のアンカーポイントを持つシェイプが必要です
|
||||
@msg The shape is too complex for a 3D effect シェイプが複雑すぎるため 3D 効果を適用できません
|
||||
@msg Extrude & Bevel 押し出しとベベル
|
||||
@msg Revolve 回転体
|
||||
@msg Inflate 膨張
|
||||
@msg Rotate 回転
|
||||
@msg Extrude & Bevel (Classic) 押し出しとベベル(クラシック)
|
||||
@msg Revolve (Classic) 回転体(クラシック)
|
||||
@msg Rotate (Classic) 回転(クラシック)
|
||||
@msg This effect この効果
|
||||
@msg clipping paths クリッピングパス
|
||||
@msg other objects その他のオブジェクト
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} には 2 つ以上のアンカーポイントを持つシェイプを含むグループが必要です
|
||||
@msg Revolve requires a group holding a shape with at least two anchors 回転体には 2 つ以上のアンカーポイントを持つシェイプを含むグループが必要です
|
||||
@msg Revolve requires a shape with at least two anchors 回転体には 2 つ以上のアンカーポイントを持つシェイプが必要です
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. グループの {label} には、パス、複合パス、テキスト、グループ、クリッピンググループ、ブレンド、シンボルインスタンスが必要です。先に次を解除または分割してください: {what}。
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects グループの {label} で扱えるオブジェクトは {MAX_MEMBERS} 個までです
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke グループまたはレイヤーの 3D 効果は、塗りや線ではなくグループ自体に適用されます
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing グループの塗りまたは線にある 3D 効果は何も描画しません
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export グループの塗りまたは線にある 3D 効果には書き出せる描画がありません
|
||||
@msg The 3D object is too complex to export 3D オブジェクトが複雑すぎるため書き出せません
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) オブジェクトに 3D 効果 (押し出しとベベル、回転体、膨張、回転) がありません
|
||||
@msg its 3D effect builds no solid 3D 効果からソリッドが作成されません
|
||||
@msg bevel must be none or one of {_1} bevel には none または {_1} のいずれかを指定してください
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} はこのアートからソリッドを作成できません。シンボルインスタンス、ブレンド、クリッピンググループに含まれるオブジェクトが多すぎます
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} はシンボル「{symbol}」のインスタンスからソリッドを作成できません。シンボルのアートを使用できません
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: クリッピングパスが複雑すぎるため、アートを切り抜けません
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} で扱えるオブジェクトは {MAX_MEMBERS} 個までです
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} は {MAX_DEPTH} 階層より深く入れ子になったアートからソリッドを作成できません
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient 回転体のペイントがこのサーフェスには複雑すぎます。分割数を減らすか、グラデーションを単純化してください
|
||||
@msg Revolve edge must be left or right 回転体: edge には left または right を指定してください
|
||||
@msg Revolve cap must be a boolean 回転体: cap はブール値にしてください
|
||||
@msg the revolved object has no fill or stroke 回転体のオブジェクトに塗りも線もありません
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor は #rrggbb 形式の不透明な RGB カラーにしてください
|
||||
@msg lights must contain one to ten lights lights には 1 から 10 個のライトを含めてください
|
||||
@msg each light must be an object 各ライトはオブジェクトにしてください
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind 不明なライトの設定です。color、intensity、rotation、height、softness、behind のいずれかを使用してください
|
||||
@msg light {key} must be a number from {min} to {max} ライト: {key} は {min} から {max} までの数値にしてください
|
||||
@msg light color must use opaque #rrggbb ライトの color は不透明な #rrggbb にしてください
|
||||
@msg light behind must be true or false ライトの behind は true または false にしてください
|
||||
@msg shadow must be true or false shadow は true または false にしてください
|
||||
@msg shadowPosition must be behind or below shadowPosition には behind または below を指定してください
|
||||
@msg {key} must be a number from {min} to {max} {key} は {min} から {max} までの数値にしてください
|
||||
@msg each of `{GRAPHICS}` must be an object `{GRAPHICS}` の各項目はオブジェクトにしてください
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` `{GRAPHICS}` の項目に指定できるのは symbol、face、x、y、scale、rotation で、`{k}` は指定できません
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol `{GRAPHICS}` の各項目にはシンボルの名前 `symbol` が必要です
|
||||
@msg `{GRAPHICS}` face must be front or back `{GRAPHICS}` の face には front または back を指定してください
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} `{GRAPHICS}` の {key} は {lo} から {hi} までの数値にしてください
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` は配列にしてください
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type グラフィックは平らな面にマッピングされます。オブジェクトの種類に押し出しまたは平面を使用してください
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` に指定できるグラフィックは {MAX_GRAPHICS} 個までです
|
||||
@msg A plane has one face: map its graphics onto the front 平面には面が 1 つしかありません。グラフィックは前面にマッピングしてください
|
||||
@msg The graphics are too complex: simplify the symbols' art グラフィックが複雑すぎます。シンボルのアートを単純化してください
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art グラフィックのアートの入れ子は {MAX_LEVELS} 階層までです。シンボルのアートを単純化してください
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art グラフィックのアートでシンボルを入れ子にできるのは {MAX_NESTING} 階層までです。シンボルのアートを単純化してください
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art グラフィックのアートに含まれるオブジェクトが多すぎます。シンボルのアートを単純化してください
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art グラフィックが描画できる塗りと線は {MAX_PIECES} 個までです。シンボルのアートを単純化してください
|
||||
@msg `surface` must be one of {_1} `surface` は {_1} のいずれかにしてください
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} はこのオブジェクトの 3D サーフェスの代わりに元のアートを描画します
|
||||
@msg {label} draws no faces for this object {label} はこのオブジェクトに面を描画しません
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges ワイヤーフレームの辺が {MAX_WIRE_EDGES} 本を超えています
|
||||
@msg The 3D surface is too complex; simplify the shape 3D サーフェスが複雑すぎます。シェイプを単純化してください
|
||||
Transform Objects オブジェクトの変形
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? `{key}` は不明なパラメーターです。`{near}` のことですか?
|
||||
@@ -6151,6 +6212,13 @@ axis Y Axis Y 軸
|
||||
@msg Batch cannot play an action with a stop バッチでは停止を含むアクションを再生できません
|
||||
Insert Menu Item… メニュー項目を挿入…
|
||||
Insert Menu Item メニュー項目を挿入
|
||||
Insert Select Path パスの選択を挿入
|
||||
Select Path パスを選択
|
||||
@msg select one whole path or compound path パス全体または複合パスを 1 つ選択してください
|
||||
@msg select only one path パスを 1 つだけ選択してください
|
||||
@msg the path is hidden or locked, or cannot be selected here パスが非表示またはロックされているか、ここでは選択できません
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one オブジェクト {value} は保存されたパスではありません: 変更されたか、別のドキュメントです
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` は "v1:" と小文字の 16 進数字 16 桁である必要があります
|
||||
Select Object… オブジェクトを選択…
|
||||
Select Object オブジェクトを選択
|
||||
Set Selection 選択範囲を設定
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1713,6 +1713,8 @@ digits Malayalam მალაიალამური
|
||||
Modified since it was read: {path} შეიცვალა წაკითხვის შემდეგ: {path}
|
||||
No Thumbnails ესკიზების გარეშე
|
||||
Nothing matches the Show filter. „ჩვენების“ ფილტრს არაფერი შეესაბამება.
|
||||
Show DCS Transparency Interactions DCS-ის გამჭვირვალობის ურთიერთქმედებების ჩვენება
|
||||
Transparent, or overlaps transparent objects: check its separations გამჭვირვალეა ან გადაფარავს გამჭვირვალე ობიექტებს: შეამოწმეთ მისი სეპარაციები
|
||||
PPI PPI
|
||||
Relink… ხელახლა დაკავშირება…
|
||||
Select an image to see its Link Info. ბმულის ინფორმაციის სანახავად მონიშნეთ გამოსახულება.
|
||||
@@ -5164,6 +5166,65 @@ font-technology Variable ცვლადი
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label}: გამოსახულებიდან მყარი სხეულის აგება შეუძლებელია. ჯერ მოახდინეთ მისი ვექტორიზაცია ბრძანებით ობიექტი › გამოსახულების ვექტორიზაცია › შექმნა.
|
||||
@msg {label} requires a shape with at least two anchors {label}: საჭიროა ფიგურა მინიმუმ ორი საყრდენი წერტილით
|
||||
@msg The shape is too complex for a 3D effect ფიგურა ზედმეტად რთულია 3D ეფექტისთვის
|
||||
@msg Extrude & Bevel ამოწევა და წაკვეთა
|
||||
@msg Revolve ბრუნვა
|
||||
@msg Inflate გაბერვა
|
||||
@msg Rotate შემობრუნება
|
||||
@msg Extrude & Bevel (Classic) ამოწევა და წაკვეთა (კლასიკური)
|
||||
@msg Revolve (Classic) ბრუნვა (კლასიკური)
|
||||
@msg Rotate (Classic) შემობრუნება (კლასიკური)
|
||||
@msg This effect ეს ეფექტი
|
||||
@msg clipping paths მომჭრელი ტრაექტორიები
|
||||
@msg other objects სხვა ობიექტები
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label}: საჭიროა ჯგუფი, რომელიც შეიცავს ფიგურას მინიმუმ ორი საყრდენი წერტილით
|
||||
@msg Revolve requires a group holding a shape with at least two anchors ბრუნვა: საჭიროა ჯგუფი, რომელიც შეიცავს ფიგურას მინიმუმ ორი საყრდენი წერტილით
|
||||
@msg Revolve requires a shape with at least two anchors ბრუნვა: საჭიროა ფიგურა მინიმუმ ორი საყრდენი წერტილით
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} ჯგუფზე: საჭიროა ტრაექტორიები, შედგენილი ტრაექტორიები, ტექსტი, ჯგუფები, მომჭრელი ჯგუფები, გადასვლები და სიმბოლოს ეგზემპლარები; ჯერ გაათავისუფლეთ ან დაშალეთ ეს ელემენტები: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} ჯგუფზე: მაქსიმუმ {MAX_MEMBERS} ობიექტი
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke ჯგუფის ან ფენის 3D ეფექტი თავად ჯგუფზე უნდა იყოს და არა მის შევსებაზე ან კონტურზე
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing ჯგუფის შევსებაზე ან კონტურზე 3D ეფექტი არაფერს ხატავს
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ჯგუფის შევსებაზე ან კონტურზე 3D ეფექტი ექსპორტისთვის არაფერს ხატავს
|
||||
@msg The 3D object is too complex to export 3D ობიექტი ზედმეტად რთულია ექსპორტისთვის
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) ობიექტს არ აქვს 3D ეფექტი (ამოწევა და წაკვეთა, ბრუნვა, გაბერვა ან შემობრუნება)
|
||||
@msg its 3D effect builds no solid მისი 3D ეფექტი სხეულს არ ქმნის
|
||||
@msg bevel must be none or one of {_1} bevel უნდა იყოს none ან ერთ-ერთი: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: ამ ნახატიდან სხეულების შექმნა შეუძლებელია: მისი სიმბოლოს ეგზემპლარები, გადასვლები და მომჭრელი ჯგუფები ზედმეტად ბევრ ობიექტს შეიცავს
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label}: სიმბოლოს „{symbol}“ ეგზემპლარიდან სხეულის შექმნა შეუძლებელია: სიმბოლოს ნახატი მიუწვდომელია
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: მომჭრელი ტრაექტორია ზედმეტად რთულია ნახატის მოსაჭრელად
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1}: მაქსიმუმ {MAX_MEMBERS} ობიექტი
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1}: შეუძლებელია სხეულების შექმნა {MAX_DEPTH} დონეზე ღრმად ჩადგმული ნახატიდან
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient ბრუნვის საღებავი ზედმეტად რთულია ამ ზედაპირისთვის; შეამცირეთ სეგმენტები ან გაამარტივეთ გრადიენტი
|
||||
@msg Revolve edge must be left or right ბრუნვა: edge უნდა იყოს left ან right
|
||||
@msg Revolve cap must be a boolean ბრუნვა: cap უნდა იყოს ლოგიკური მნიშვნელობა
|
||||
@msg the revolved object has no fill or stroke მობრუნებულ ობიექტს არ აქვს შევსება ან კონტური
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor უნდა იყოს გაუმჭვირვალე RGB ფერი #rrggbb ფორმით
|
||||
@msg lights must contain one to ten lights lights უნდა შეიცავდეს ერთიდან ათამდე სინათლეს
|
||||
@msg each light must be an object თითოეული სინათლე უნდა იყოს ობიექტი
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind სინათლის უცნობი პარამეტრი: გამოიყენეთ color, intensity, rotation, height, softness ან behind
|
||||
@msg light {key} must be a number from {min} to {max} სინათლე: {key} უნდა იყოს რიცხვი {min}-დან {max}-მდე
|
||||
@msg light color must use opaque #rrggbb სინათლის color უნდა იყოს გაუმჭვირვალე #rrggbb
|
||||
@msg light behind must be true or false სინათლის behind უნდა იყოს true ან false
|
||||
@msg shadow must be true or false shadow უნდა იყოს true ან false
|
||||
@msg shadowPosition must be behind or below shadowPosition უნდა იყოს behind ან below
|
||||
@msg {key} must be a number from {min} to {max} {key} უნდა იყოს რიცხვი {min}-დან {max}-მდე
|
||||
@msg each of `{GRAPHICS}` must be an object `{GRAPHICS}`-ის თითოეული ჩანაწერი უნდა იყოს ობიექტი
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` `{GRAPHICS}`-ის ჩანაწერები იღებს symbol, face, x, y, scale და rotation მნიშვნელობებს და არა `{k}`-ს
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol `{GRAPHICS}`-ის თითოეულ ჩანაწერს სჭირდება `symbol`, სიმბოლოს სახელი
|
||||
@msg `{GRAPHICS}` face must be front or back `{GRAPHICS}`-ის face უნდა იყოს front ან back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} `{GRAPHICS}`-ის {key} უნდა იყოს რიცხვი {lo}-დან {hi}-მდე
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` უნდა იყოს მასივი
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type გრაფიკა ბრტყელ წახნაგებზე თავსდება: გამოიყენეთ ობიექტის ტიპი „ამოწევა“ ან „სიბრტყე“
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` იღებს მაქსიმუმ {MAX_GRAPHICS} გრაფიკას
|
||||
@msg A plane has one face: map its graphics onto the front სიბრტყეს ერთი მხარე აქვს: მისი გრაფიკა წინა მხარეზე განათავსეთ
|
||||
@msg The graphics are too complex: simplify the symbols' art გრაფიკა ზედმეტად რთულია: გაამარტივეთ სიმბოლოების ნახატი
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art გრაფიკის ნახატი შეიძლება ჩადგმული იყოს მაქსიმუმ {MAX_LEVELS} დონეზე: გაამარტივეთ სიმბოლოს ნახატი
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art გრაფიკის ნახატში სიმბოლოები შეიძლება ჩადგმული იყოს მაქსიმუმ {MAX_NESTING} დონეზე: გაამარტივეთ სიმბოლოს ნახატი
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art გრაფიკის ნახატი ზედმეტად ბევრ ობიექტს შეიცავს: გაამარტივეთ სიმბოლოს ნახატი
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art გრაფიკა ხატავს მაქსიმუმ {MAX_PIECES} შევსებას და კონტურს: გაამარტივეთ სიმბოლოს ნახატი
|
||||
@msg `surface` must be one of {_1} `surface` უნდა იყოს ერთ-ერთი: {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label}: ამ ობიექტისთვის 3D ზედაპირის ნაცვლად საწყისი ნახატი იხატება
|
||||
@msg {label} draws no faces for this object {label}: ამ ობიექტისთვის არცერთი წახნაგი არ იხატება
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges კარკასს {MAX_WIRE_EDGES}-ზე მეტი წიბო აქვს
|
||||
@msg The 3D surface is too complex; simplify the shape 3D ზედაპირი ზედმეტად რთულია; გაამარტივეთ ფიგურა
|
||||
Transform Objects ობიექტების გარდაქმნა
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? უცნობი პარამეტრი `{key}`; იქნებ `{near}` იგულისხმეთ?
|
||||
@@ -6412,6 +6473,13 @@ axis Y Axis Y ღერძი
|
||||
@msg Batch cannot play an action with a stop პაკეტურ დამუშავებას არ შეუძლია გაჩერების მქონე მოქმედების შესრულება
|
||||
Insert Menu Item… მენიუს ელემენტის ჩასმა…
|
||||
Insert Menu Item მენიუს ელემენტის ჩასმა
|
||||
Insert Select Path ტრაექტორიის მონიშვნის ჩასმა
|
||||
Select Path ტრაექტორიის მონიშვნა
|
||||
@msg select one whole path or compound path მონიშნეთ ერთი მთლიანი ტრაექტორია ან შედგენილი ტრაექტორია
|
||||
@msg select only one path მონიშნეთ მხოლოდ ერთი ტრაექტორია
|
||||
@msg the path is hidden or locked, or cannot be selected here ტრაექტორია დამალული ან ჩაკეტილია, ან აქ ვერ მოინიშნება
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one ობიექტი {value} არ არის შენახული ტრაექტორია: ის შეიცვალა, ან დოკუმენტი სხვაა
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` უნდა იყოს "v1:" და 16 პატარა თექვსმეტობითი ციფრი
|
||||
Select Object… ობიექტის მონიშვნა…
|
||||
Select Object ობიექტის მონიშვნა
|
||||
Set Selection მონიშვნის დაყენება
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2483,6 +2483,8 @@ antiAlias Sharp 날카롭게
|
||||
Normal 표준
|
||||
Note: 참고:
|
||||
Nothing matches the Show filter. 표시 필터와 일치하는 항목이 없습니다.
|
||||
Show DCS Transparency Interactions DCS 투명도 상호 작용 표시
|
||||
Transparent, or overlaps transparent objects: check its separations 투명하거나 투명한 개체와 겹칩니다: 분판을 확인하세요
|
||||
Nothing to print 인쇄할 항목 없음
|
||||
Nudge 미세 이동
|
||||
Nudge in Perspective 원근 미세 이동
|
||||
@@ -5454,6 +5456,65 @@ lighting Light 광원
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label}은(는) 이미지로 입체를 만들 수 없습니다. 먼저 개체 › 이미지 추적 › 만들기로 추적하십시오.
|
||||
@msg {label} requires a shape with at least two anchors {label}에는 고정점이 두 개 이상인 모양이 필요합니다
|
||||
@msg The shape is too complex for a 3D effect 모양이 너무 복잡하여 3D 효과를 적용할 수 없습니다
|
||||
@msg Extrude & Bevel 돌출 및 경사
|
||||
@msg Revolve 회전체
|
||||
@msg Inflate 부풀리기
|
||||
@msg Rotate 회전
|
||||
@msg Extrude & Bevel (Classic) 돌출 및 경사(클래식)
|
||||
@msg Revolve (Classic) 회전체(클래식)
|
||||
@msg Rotate (Classic) 회전(클래식)
|
||||
@msg This effect 이 효과
|
||||
@msg clipping paths 클리핑 패스
|
||||
@msg other objects 기타 개체
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label}에는 고정점이 두 개 이상인 모양을 포함한 그룹이 필요합니다
|
||||
@msg Revolve requires a group holding a shape with at least two anchors 회전체에는 고정점이 두 개 이상인 모양을 포함한 그룹이 필요합니다
|
||||
@msg Revolve requires a shape with at least two anchors 회전체에는 고정점이 두 개 이상인 모양이 필요합니다
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. 그룹의 {label}에는 패스, 컴파운드 패스, 텍스트, 그룹, 클리핑 그룹, 블렌드, 심볼 인스턴스가 필요합니다. 먼저 다음을 놓거나 확장하십시오: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects 그룹의 {label}은(는) 개체를 최대 {MAX_MEMBERS}개까지 받습니다
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke 그룹이나 레이어의 3D 효과는 칠이나 획이 아니라 그룹 자체에 적용됩니다
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing 그룹의 칠이나 획에 있는 3D 효과는 아무것도 그리지 않습니다
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 그룹의 칠이나 획에 있는 3D 효과는 내보낼 것을 그리지 않습니다
|
||||
@msg The 3D object is too complex to export 3D 개체가 너무 복잡하여 내보낼 수 없습니다
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) 개체에 3D 효과(돌출 및 경사, 회전체, 부풀리기 또는 회전)가 없습니다
|
||||
@msg its 3D effect builds no solid 3D 효과가 입체를 만들지 않습니다
|
||||
@msg bevel must be none or one of {_1} bevel은 none 또는 {_1} 중 하나여야 합니다
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}은(는) 이 아트로 입체를 만들 수 없습니다. 심볼 인스턴스, 블렌드, 클리핑 그룹에 개체가 너무 많습니다
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label}은(는) 심볼 “{symbol}”의 인스턴스로 입체를 만들 수 없습니다. 심볼의 아트를 사용할 수 없습니다
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: 클리핑 패스가 너무 복잡하여 아트를 자를 수 없습니다
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1}은(는) 개체를 최대 {MAX_MEMBERS}개까지 받습니다
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1}은(는) {MAX_DEPTH}단계보다 깊이 중첩된 아트로 입체를 만들 수 없습니다
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient 회전체의 칠이 이 표면에 비해 너무 복잡합니다. 세그먼트를 줄이거나 그레이디언트를 단순화하십시오
|
||||
@msg Revolve edge must be left or right 회전체: edge는 left 또는 right여야 합니다
|
||||
@msg Revolve cap must be a boolean 회전체: cap은 부울 값이어야 합니다
|
||||
@msg the revolved object has no fill or stroke 회전체 개체에 칠이나 획이 없습니다
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor는 #rrggbb 형식의 불투명한 RGB 색상이어야 합니다
|
||||
@msg lights must contain one to ten lights lights에는 조명이 1개에서 10개까지 있어야 합니다
|
||||
@msg each light must be an object 각 조명은 개체여야 합니다
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind 알 수 없는 조명 설정입니다. color, intensity, rotation, height, softness 또는 behind를 사용하십시오
|
||||
@msg light {key} must be a number from {min} to {max} 조명: {key}은(는) {min}에서 {max} 사이의 숫자여야 합니다
|
||||
@msg light color must use opaque #rrggbb 조명의 color는 불투명한 #rrggbb여야 합니다
|
||||
@msg light behind must be true or false 조명의 behind는 true 또는 false여야 합니다
|
||||
@msg shadow must be true or false shadow는 true 또는 false여야 합니다
|
||||
@msg shadowPosition must be behind or below shadowPosition은 behind 또는 below여야 합니다
|
||||
@msg {key} must be a number from {min} to {max} {key}은(는) {min}에서 {max} 사이의 숫자여야 합니다
|
||||
@msg each of `{GRAPHICS}` must be an object `{GRAPHICS}`의 각 항목은 개체여야 합니다
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` `{GRAPHICS}` 항목에는 symbol, face, x, y, scale, rotation만 사용할 수 있으며 `{k}`은(는) 사용할 수 없습니다
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol `{GRAPHICS}`의 각 항목에는 심볼 이름인 `symbol`이 필요합니다
|
||||
@msg `{GRAPHICS}` face must be front or back `{GRAPHICS}`의 face는 front 또는 back이어야 합니다
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} `{GRAPHICS}`의 {key}은(는) {lo}에서 {hi} 사이의 숫자여야 합니다
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}`은(는) 배열이어야 합니다
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type 그래픽은 평평한 면에 매핑됩니다. 돌출 또는 평면 개체 유형을 사용하십시오
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}`에는 그래픽을 최대 {MAX_GRAPHICS}개까지 넣을 수 있습니다
|
||||
@msg A plane has one face: map its graphics onto the front 평면에는 면이 하나뿐입니다. 그래픽을 앞면에 매핑하십시오
|
||||
@msg The graphics are too complex: simplify the symbols' art 그래픽이 너무 복잡합니다. 심볼의 아트를 단순화하십시오
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art 그래픽의 아트는 최대 {MAX_LEVELS}단계까지 중첩할 수 있습니다. 심볼의 아트를 단순화하십시오
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art 그래픽의 아트는 심볼을 최대 {MAX_NESTING}단계까지 중첩할 수 있습니다. 심볼의 아트를 단순화하십시오
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art 그래픽의 아트에 개체가 너무 많습니다. 심볼의 아트를 단순화하십시오
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art 그래픽은 칠과 획을 최대 {MAX_PIECES}개까지 그립니다. 심볼의 아트를 단순화하십시오
|
||||
@msg `surface` must be one of {_1} `surface`은(는) {_1} 중 하나여야 합니다
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label}은(는) 이 개체에 3D 표면 대신 원본 아트를 그립니다
|
||||
@msg {label} draws no faces for this object {label}은(는) 이 개체에 면을 그리지 않습니다
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges 와이어프레임의 가장자리가 {MAX_WIRE_EDGES}개를 넘습니다
|
||||
@msg The 3D surface is too complex; simplify the shape 3D 표면이 너무 복잡합니다. 모양을 단순화하십시오
|
||||
Transform Objects 개체 변형
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? 알 수 없는 매개변수 `{key}`: `{near}`을(를) 의미하셨습니까?
|
||||
@@ -6047,6 +6108,13 @@ axis Y Axis Y축
|
||||
@msg Batch cannot play an action with a stop 일괄 처리에서는 중지가 있는 액션을 재생할 수 없습니다
|
||||
Insert Menu Item… 메뉴 항목 삽입…
|
||||
Insert Menu Item 메뉴 항목 삽입
|
||||
Insert Select Path 패스 선택 삽입
|
||||
Select Path 패스 선택
|
||||
@msg select one whole path or compound path 전체 패스 또는 컴파운드 패스를 하나 선택하세요
|
||||
@msg select only one path 패스를 하나만 선택하세요
|
||||
@msg the path is hidden or locked, or cannot be selected here 패스가 숨겨져 있거나 잠겨 있거나 여기에서 선택할 수 없습니다
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one 개체 {value}은(는) 저장된 패스가 아닙니다: 변경되었거나 다른 문서입니다
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint`는 "v1:"과 소문자 16진수 16자리여야 합니다
|
||||
Select Object… 개체 선택…
|
||||
Select Object 개체 선택
|
||||
Set Selection 선택 영역 설정
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1789,6 +1789,8 @@ digits Malayalam Malayalam
|
||||
Modified since it was read: {path} Gewijzigd sinds het is ingelezen: {path}
|
||||
No Thumbnails Geen miniaturen
|
||||
Nothing matches the Show filter. Niets komt overeen met het weergavefilter.
|
||||
Show DCS Transparency Interactions DCS-transparantie-interacties weergeven
|
||||
Transparent, or overlaps transparent objects: check its separations Transparant of overlapt transparante objecten: controleer de scheidingen
|
||||
PPI PPI
|
||||
Relink… Opnieuw koppelen…
|
||||
Find Linked Files Gekoppelde bestanden zoeken
|
||||
@@ -6055,6 +6057,13 @@ axis Y Axis Y-as
|
||||
@msg Batch cannot play an action with a stop Batch kan geen actie met een stop afspelen
|
||||
Insert Menu Item… Menu-item invoegen…
|
||||
Insert Menu Item Menu-item invoegen
|
||||
Insert Select Path Pad selecteren invoegen
|
||||
Select Path Pad selecteren
|
||||
@msg select one whole path or compound path selecteer één volledig pad of samengesteld pad
|
||||
@msg select only one path selecteer slechts één pad
|
||||
@msg the path is hidden or locked, or cannot be selected here het pad is verborgen of vergrendeld, of kan hier niet worden geselecteerd
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one object {value} is niet het opgeslagen pad: het is gewijzigd, of het document is een ander
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` moet "v1:" zijn, gevolgd door 16 hexadecimale cijfers in kleine letters
|
||||
Select Object… Object selecteren…
|
||||
Select Object Object selecteren
|
||||
Set Selection Selectie instellen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1878,6 +1878,8 @@ digits Malayalam Malajalam
|
||||
Modified since it was read: {path} Zmodyfikowano od wczytania: {path}
|
||||
No Thumbnails Bez miniatur
|
||||
Nothing matches the Show filter. Nic nie pasuje do filtra Pokaż.
|
||||
Show DCS Transparency Interactions Pokaż interakcje przezroczystości DCS
|
||||
Transparent, or overlaps transparent objects: check its separations Przezroczysty lub nakłada się na przezroczyste obiekty: sprawdź jego separacje
|
||||
PPI PPI
|
||||
Relink… Połącz ponownie…
|
||||
Find Linked Files Znajdź połączone pliki
|
||||
@@ -5706,6 +5708,65 @@ mark-style Japanese Japońskie
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} nie może utworzyć bryły z obrazu: najpierw obrysuj go poleceniem Obiekt › Obrys obrazu › Utwórz.
|
||||
@msg {label} requires a shape with at least two anchors {label} wymaga kształtu z co najmniej dwoma punktami
|
||||
@msg The shape is too complex for a 3D effect Kształt jest zbyt złożony dla efektu 3D
|
||||
@msg Extrude & Bevel Wyciągnięcie i fazowanie
|
||||
@msg Revolve Obrót
|
||||
@msg Inflate Nadmuchanie
|
||||
@msg Rotate Obróć
|
||||
@msg Extrude & Bevel (Classic) Wyciągnięcie i fazowanie (klasyczne)
|
||||
@msg Revolve (Classic) Obrót (klasyczny)
|
||||
@msg Rotate (Classic) Obróć (klasyczny)
|
||||
@msg This effect Ten efekt
|
||||
@msg clipping paths ścieżki przycinające
|
||||
@msg other objects inne obiekty
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} wymaga grupy zawierającej kształt z co najmniej dwoma punktami
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Obrót wymaga grupy zawierającej kształt z co najmniej dwoma punktami
|
||||
@msg Revolve requires a shape with at least two anchors Obrót wymaga kształtu z co najmniej dwoma punktami
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} na grupie wymaga ścieżek, ścieżek złożonych, tekstu, grup, grup przycinających, przejść i wystąpień symboli: najpierw zwolnij lub rozszerz te elementy: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} na grupie przyjmuje najwyżej {MAX_MEMBERS} obiektów
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke Efekt 3D na grupie lub warstwie należy nałożyć na samą grupę, a nie na jej wypełnienie lub obrys
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing efekt 3D na wypełnieniu lub obrysie grupy niczego nie rysuje
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export efekt 3D na wypełnieniu lub obrysie grupy nie rysuje niczego do eksportu
|
||||
@msg The 3D object is too complex to export Obiekt 3D jest zbyt złożony, aby go wyeksportować
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) obiekt nie ma efektu 3D (Wyciągnięcie i fazowanie, Obrót, Nadmuchanie ani Obróć)
|
||||
@msg its 3D effect builds no solid jego efekt 3D nie tworzy żadnej bryły
|
||||
@msg bevel must be none or one of {_1} bevel musi mieć wartość none lub jedną z: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} nie może utworzyć brył z tego rysunku: jego wystąpienia symboli, przejścia i grupy przycinające zawierają zbyt wiele obiektów
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} nie może utworzyć bryły z wystąpienia symbolu „{symbol}”: rysunek symbolu jest niedostępny
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: ścieżka przycinająca jest zbyt złożona, aby przyciąć nią rysunek
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} przyjmuje najwyżej {MAX_MEMBERS} obiektów
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} nie może utworzyć brył z rysunku zagnieżdżonego na więcej niż {MAX_DEPTH} poziomach
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient Farba efektu Obrót jest zbyt złożona dla tej powierzchni; zmniejsz Segmenty lub uprość gradient
|
||||
@msg Revolve edge must be left or right Obrót: edge musi mieć wartość left lub right
|
||||
@msg Revolve cap must be a boolean Obrót: cap musi być wartością logiczną
|
||||
@msg the revolved object has no fill or stroke obracany obiekt nie ma wypełnienia ani obrysu
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor musi być nieprzezroczystym kolorem RGB w postaci #rrggbb
|
||||
@msg lights must contain one to ten lights lights musi zawierać od jednego do dziesięciu świateł
|
||||
@msg each light must be an object każde światło musi być obiektem
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind nieznane ustawienie światła: użyj color, intensity, rotation, height, softness lub behind
|
||||
@msg light {key} must be a number from {min} to {max} światło: {key} musi być liczbą od {min} do {max}
|
||||
@msg light color must use opaque #rrggbb color światła musi być nieprzezroczystym kolorem #rrggbb
|
||||
@msg light behind must be true or false behind światła musi mieć wartość true lub false
|
||||
@msg shadow must be true or false shadow musi mieć wartość true lub false
|
||||
@msg shadowPosition must be behind or below shadowPosition musi mieć wartość behind lub below
|
||||
@msg {key} must be a number from {min} to {max} {key} musi być liczbą od {min} do {max}
|
||||
@msg each of `{GRAPHICS}` must be an object każdy element `{GRAPHICS}` musi być obiektem
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` elementy `{GRAPHICS}` przyjmują symbol, face, x, y, scale i rotation, a nie `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol każdy element `{GRAPHICS}` wymaga `symbol`, nazwy symbolu
|
||||
@msg `{GRAPHICS}` face must be front or back face w `{GRAPHICS}` musi mieć wartość front lub back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} {key} w `{GRAPHICS}` musi być liczbą od {lo} do {hi}
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` musi być tablicą
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Grafiki są nakładane na płaskie ściany: użyj typu obiektu Wyciągnięcie lub Płaszczyzna
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` przyjmuje najwyżej {MAX_GRAPHICS} grafik
|
||||
@msg A plane has one face: map its graphics onto the front Płaszczyzna ma jedną stronę: umieść jej grafiki na przedniej stronie
|
||||
@msg The graphics are too complex: simplify the symbols' art Grafiki są zbyt złożone: uprość rysunki symboli
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art Rysunek grafiki może być zagnieżdżony najwyżej na {MAX_LEVELS} poziomach: uprość rysunek symbolu
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art Rysunek grafiki może zagnieżdżać symbole najwyżej na {MAX_NESTING} poziomach: uprość rysunek symbolu
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art Rysunek grafiki zawiera zbyt wiele obiektów: uprość rysunek symbolu
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Grafika rysuje najwyżej {MAX_PIECES} wypełnień i obrysów: uprość rysunek symbolu
|
||||
@msg `surface` must be one of {_1} `surface` musi mieć jedną z wartości: {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} rysuje dla tego obiektu rysunek źródłowy zamiast jego powierzchni 3D
|
||||
@msg {label} draws no faces for this object {label} nie rysuje żadnych ścian dla tego obiektu
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges model szkieletowy ma więcej niż {MAX_WIRE_EDGES} krawędzi
|
||||
@msg The 3D surface is too complex; simplify the shape Powierzchnia 3D jest zbyt złożona; uprość kształt
|
||||
Add Guides Dodaj linie pomocnicze
|
||||
Total: Razem:
|
||||
@@ -6315,6 +6376,13 @@ axis Y Axis Oś Y
|
||||
@msg Batch cannot play an action with a stop Przetwarzanie wsadowe nie może odtworzyć operacji z zatrzymaniem
|
||||
Insert Menu Item… Wstaw polecenie menu…
|
||||
Insert Menu Item Wstaw polecenie menu
|
||||
Insert Select Path Wstaw zaznaczenie ścieżki
|
||||
Select Path Zaznacz ścieżkę
|
||||
@msg select one whole path or compound path zaznacz jedną całą ścieżkę lub ścieżkę złożoną
|
||||
@msg select only one path zaznacz tylko jedną ścieżkę
|
||||
@msg the path is hidden or locked, or cannot be selected here ścieżka jest ukryta lub zablokowana albo nie można jej tu zaznaczyć
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one obiekt {value} nie jest zapisaną ścieżką: zmienił się albo dokument jest inny
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` musi mieć postać "v1:" i 16 cyfr szesnastkowych małymi literami
|
||||
Select Object… Zaznacz obiekt…
|
||||
Select Object Zaznacz obiekt
|
||||
Set Selection Ustaw zaznaczenie
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1877,6 +1877,8 @@ antiAlias Sharp Nítido
|
||||
Normal Screen Mode Modo de Tela Normal
|
||||
Note: Nota:
|
||||
Nothing matches the Show filter. Nada corresponde ao filtro Mostrar.
|
||||
Show DCS Transparency Interactions Mostrar interações de transparência DCS
|
||||
Transparent, or overlaps transparent objects: check its separations Transparente ou sobrepõe objetos transparentes: verifique as separações
|
||||
Nothing to print Nada para imprimir
|
||||
Nudge Nudge
|
||||
Nudge in Perspective Nudge em Perspectiva
|
||||
@@ -5825,6 +5827,65 @@ mark-style Japanese Japonesas
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} não pode criar um sólido a partir de uma imagem: rastreie-a antes com Objeto › Rastreamento de Imagem › Criar.
|
||||
@msg {label} requires a shape with at least two anchors {label} exige uma forma com pelo menos dois pontos de ancoragem
|
||||
@msg The shape is too complex for a 3D effect A forma é complexa demais para um efeito 3D
|
||||
@msg Extrude & Bevel Extrusão e Bisel
|
||||
@msg Revolve Revolver
|
||||
@msg Inflate Inflar
|
||||
@msg Rotate Girar
|
||||
@msg Extrude & Bevel (Classic) Extrusão e Bisel (Clássico)
|
||||
@msg Revolve (Classic) Revolver (Clássico)
|
||||
@msg Rotate (Classic) Girar (Clássico)
|
||||
@msg This effect Este efeito
|
||||
@msg clipping paths caminhos de recorte
|
||||
@msg other objects outros objetos
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} exige um grupo com uma forma com pelo menos dois pontos de ancoragem
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Revolver exige um grupo com uma forma com pelo menos dois pontos de ancoragem
|
||||
@msg Revolve requires a shape with at least two anchors Revolver exige uma forma com pelo menos dois pontos de ancoragem
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} em um grupo exige caminhos, caminhos compostos, texto, grupos, grupos de recorte, mesclagens e instâncias de símbolo: primeiro libere ou expanda estes itens: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} em um grupo aceita no máximo {MAX_MEMBERS} objetos
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke Um efeito 3D em um grupo ou camada se aplica ao próprio grupo, não ao seu preenchimento ou traçado
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing um efeito 3D no preenchimento ou traçado de um grupo não desenha nada
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export um efeito 3D no preenchimento ou traçado de um grupo não desenha nada para exportar
|
||||
@msg The 3D object is too complex to export O objeto 3D é complexo demais para ser exportado
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) o objeto não tem efeito 3D (Extrusão e Bisel, Revolver, Inflar ou Girar)
|
||||
@msg its 3D effect builds no solid seu efeito 3D não constrói nenhum sólido
|
||||
@msg bevel must be none or one of {_1} bevel deve ser none ou um de {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} não pode construir sólidos a partir desta arte: suas instâncias de símbolo, mesclagens e grupos de recorte contêm objetos demais
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} não pode construir um sólido a partir da instância do símbolo “{symbol}”: a arte do símbolo não está disponível
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: o caminho de recorte é complexo demais para recortar a arte
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} aceita no máximo {MAX_MEMBERS} objetos
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} não pode construir sólidos a partir de arte aninhada em mais de {MAX_DEPTH} níveis
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient A pintura de Revolver é complexa demais para esta superfície; reduza Segmentos ou simplifique o degradê
|
||||
@msg Revolve edge must be left or right Revolver: edge deve ser left ou right
|
||||
@msg Revolve cap must be a boolean Revolver: cap deve ser um booleano
|
||||
@msg the revolved object has no fill or stroke o objeto revolvido não tem preenchimento nem traçado
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor deve ser uma cor RGB opaca no formato #rrggbb
|
||||
@msg lights must contain one to ten lights lights deve conter de uma a dez luzes
|
||||
@msg each light must be an object cada luz deve ser um objeto
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind configuração de luz desconhecida: use color, intensity, rotation, height, softness ou behind
|
||||
@msg light {key} must be a number from {min} to {max} luz: {key} deve ser um número de {min} a {max}
|
||||
@msg light color must use opaque #rrggbb color de uma luz deve usar #rrggbb opaco
|
||||
@msg light behind must be true or false behind de uma luz deve ser true ou false
|
||||
@msg shadow must be true or false shadow deve ser true ou false
|
||||
@msg shadowPosition must be behind or below shadowPosition deve ser behind ou below
|
||||
@msg {key} must be a number from {min} to {max} {key} deve ser um número de {min} a {max}
|
||||
@msg each of `{GRAPHICS}` must be an object cada item de `{GRAPHICS}` deve ser um objeto
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` as entradas de `{GRAPHICS}` aceitam symbol, face, x, y, scale e rotation, não `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol cada item de `{GRAPHICS}` precisa de `symbol`, o nome de um símbolo
|
||||
@msg `{GRAPHICS}` face must be front or back face de `{GRAPHICS}` deve ser front ou back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} {key} de `{GRAPHICS}` deve ser um número de {lo} a {hi}
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` deve ser uma matriz
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Os gráficos são mapeados em faces planas: use o tipo de objeto Extrusão ou Plano
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` aceita no máximo {MAX_GRAPHICS} gráficos
|
||||
@msg A plane has one face: map its graphics onto the front Um plano tem uma única face: mapeie seus gráficos na face frontal
|
||||
@msg The graphics are too complex: simplify the symbols' art Os gráficos são complexos demais: simplifique a arte dos símbolos
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art A arte de um gráfico pode ser aninhada em no máximo {MAX_LEVELS} níveis: simplifique a arte do símbolo
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art A arte de um gráfico pode aninhar símbolos em no máximo {MAX_NESTING} níveis: simplifique a arte do símbolo
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art A arte de um gráfico contém objetos demais: simplifique a arte do símbolo
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Um gráfico desenha no máximo {MAX_PIECES} preenchimentos e traçados: simplifique a arte do símbolo
|
||||
@msg `surface` must be one of {_1} `surface` deve ser um de {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} desenha a arte original deste objeto em vez da sua superfície 3D
|
||||
@msg {label} draws no faces for this object {label} não desenha nenhuma face para este objeto
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges a estrutura de arame tem mais de {MAX_WIRE_EDGES} arestas
|
||||
@msg The 3D surface is too complex; simplify the shape A superfície 3D é complexa demais; simplifique a forma
|
||||
@msg an image of {w} × {h} pixels is too large to draw uma imagem de {w} × {h} pixels é grande demais para ser desenhada
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: não é uma imagem PNG, JPEG, WebP, TIFF ou BMP
|
||||
@@ -6376,6 +6437,13 @@ axis Y Axis Eixo Y
|
||||
@msg Batch cannot play an action with a stop Lote não pode reproduzir uma ação com uma parada
|
||||
Insert Menu Item… Inserir Item de Menu…
|
||||
Insert Menu Item Inserir Item de Menu
|
||||
Insert Select Path Inserir Seleção de Caminho
|
||||
Select Path Selecionar Caminho
|
||||
@msg select one whole path or compound path selecione um caminho inteiro ou um caminho composto
|
||||
@msg select only one path selecione apenas um caminho
|
||||
@msg the path is hidden or locked, or cannot be selected here o caminho está oculto ou bloqueado, ou não pode ser selecionado aqui
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one o objeto {value} não é o caminho armazenado: ele mudou, ou o documento é outro
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` deve ser "v1:" e 16 dígitos hexadecimais minúsculos
|
||||
Select Object… Selecionar Objeto…
|
||||
Select Object Selecionar Objeto
|
||||
Set Selection Definir Seleção
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2301,6 +2301,8 @@ file-filter Templates Шаблоны
|
||||
Normal Screen Mode Обычный режим экрана
|
||||
Note: Примечание:
|
||||
Nothing matches the Show filter. Ничего не соответствует фильтру «Показать».
|
||||
Show DCS Transparency Interactions Показывать взаимодействия DCS с прозрачностью
|
||||
Transparent, or overlaps transparent objects: check its separations Прозрачно или перекрывает прозрачные объекты: проверьте цветоделение
|
||||
Nothing to print Нечего печатать
|
||||
Nudge Сдвинуть
|
||||
Nudge in Perspective Сдвинуть в перспективе
|
||||
@@ -5479,6 +5481,65 @@ lighting Light Свет
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label}: из изображения нельзя построить тело. Сначала трассируйте его командой Объект › Трассировка изображения › Создать.
|
||||
@msg {label} requires a shape with at least two anchors {label}: требуется фигура не менее чем с двумя опорными точками
|
||||
@msg The shape is too complex for a 3D effect Фигура слишком сложна для 3D-эффекта
|
||||
@msg Extrude & Bevel Выдавливание и фаска
|
||||
@msg Revolve Вращение
|
||||
@msg Inflate Надувание
|
||||
@msg Rotate Повернуть
|
||||
@msg Extrude & Bevel (Classic) Выдавливание и фаска (классическое)
|
||||
@msg Revolve (Classic) Вращение (классическое)
|
||||
@msg Rotate (Classic) Поворот (классический)
|
||||
@msg This effect Этот эффект
|
||||
@msg clipping paths обтравочные контуры
|
||||
@msg other objects другие объекты
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label}: требуется группа с фигурой не менее чем с двумя опорными точками
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Вращение: требуется группа с фигурой не менее чем с двумя опорными точками
|
||||
@msg Revolve requires a shape with at least two anchors Вращение: требуется фигура не менее чем с двумя опорными точками
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} для группы: нужны контуры, составные контуры, текст, группы, обтравочные группы, переходы и экземпляры символов; сначала отмените или разберите эти элементы: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} для группы: допускается не более {MAX_MEMBERS} объектов
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke 3D-эффект для группы или слоя применяется к самой группе, а не к её заливке или обводке
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing 3D-эффект на заливке или обводке группы ничего не рисует
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 3D-эффект на заливке или обводке группы не рисует ничего для экспорта
|
||||
@msg The 3D object is too complex to export 3D-объект слишком сложен для экспорта
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) у объекта нет 3D-эффекта (Выдавливание и фаска, Вращение, Надувание или Повернуть)
|
||||
@msg its 3D effect builds no solid его 3D-эффект не строит тела
|
||||
@msg bevel must be none or one of {_1} bevel должен быть none или одним из: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: нельзя построить тела из этой иллюстрации: её экземпляры символов, переходы и обтравочные группы содержат слишком много объектов
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label}: нельзя построить тело из экземпляра символа «{symbol}»: иллюстрация символа недоступна
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: обтравочный контур слишком сложен, чтобы обрезать им иллюстрацию
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1}: допускается не более {MAX_MEMBERS} объектов
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1}: нельзя построить тела из иллюстрации с вложенностью более {MAX_DEPTH} уровней
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient Окраска «Вращения» слишком сложна для этой поверхности; уменьшите «Сегменты» или упростите градиент
|
||||
@msg Revolve edge must be left or right Вращение: edge должен быть left или right
|
||||
@msg Revolve cap must be a boolean Вращение: cap должен быть логическим значением
|
||||
@msg the revolved object has no fill or stroke у объекта вращения нет заливки или обводки
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor должен быть непрозрачным цветом RGB в формате #rrggbb
|
||||
@msg lights must contain one to ten lights lights должен содержать от одного до десяти источников света
|
||||
@msg each light must be an object каждый источник света должен быть объектом
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind неизвестный параметр источника света: используйте color, intensity, rotation, height, softness или behind
|
||||
@msg light {key} must be a number from {min} to {max} источник света: {key} должен быть числом от {min} до {max}
|
||||
@msg light color must use opaque #rrggbb color источника света должен быть непрозрачным #rrggbb
|
||||
@msg light behind must be true or false behind источника света должен быть true или false
|
||||
@msg shadow must be true or false shadow должен быть true или false
|
||||
@msg shadowPosition must be behind or below shadowPosition должен быть behind или below
|
||||
@msg {key} must be a number from {min} to {max} {key} должно быть числом от {min} до {max}
|
||||
@msg each of `{GRAPHICS}` must be an object каждый элемент `{GRAPHICS}` должен быть объектом
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` элементы `{GRAPHICS}` принимают symbol, face, x, y, scale и rotation, а не `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol каждому элементу `{GRAPHICS}` нужен `symbol`, имя символа
|
||||
@msg `{GRAPHICS}` face must be front or back face в `{GRAPHICS}` должен быть front или back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} {key} в `{GRAPHICS}` должен быть числом от {lo} до {hi}
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` должен быть массивом
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Графика наносится на плоские грани: используйте тип объекта «Выдавливание» или «Плоскость»
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` принимает не более {MAX_GRAPHICS} графических элементов
|
||||
@msg A plane has one face: map its graphics onto the front У плоскости одна сторона: размещайте её графику на передней стороне
|
||||
@msg The graphics are too complex: simplify the symbols' art Графика слишком сложна: упростите иллюстрации символов
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art Иллюстрация графики может быть вложена не более чем на {MAX_LEVELS} уровней: упростите иллюстрацию символа
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art Иллюстрация графики может вкладывать символы не более чем на {MAX_NESTING} уровней: упростите иллюстрацию символа
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art Иллюстрация графики содержит слишком много объектов: упростите иллюстрацию символа
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Графика рисует не более {MAX_PIECES} заливок и обводок: упростите иллюстрацию символа
|
||||
@msg `surface` must be one of {_1} `surface` должно быть одним из: {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label}: для этого объекта рисуется исходная иллюстрация вместо его 3D-поверхности
|
||||
@msg {label} draws no faces for this object {label}: для этого объекта не рисуется ни одной грани
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges каркас содержит более {MAX_WIRE_EDGES} рёбер
|
||||
@msg The 3D surface is too complex; simplify the shape 3D-поверхность слишком сложна; упростите фигуру
|
||||
Transform Objects Трансформировать объекты
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? неизвестный параметр `{key}`; возможно, имелся в виду `{near}`?
|
||||
@@ -6072,6 +6133,13 @@ axis Y Axis Ось Y
|
||||
@msg Batch cannot play an action with a stop Пакет не может воспроизвести действие с остановкой
|
||||
Insert Menu Item… Вставить пункт меню…
|
||||
Insert Menu Item Вставить пункт меню
|
||||
Insert Select Path Вставить выделение контура
|
||||
Select Path Выделить контур
|
||||
@msg select one whole path or compound path выделите один целый контур или составной контур
|
||||
@msg select only one path выделите только один контур
|
||||
@msg the path is hidden or locked, or cannot be selected here контур скрыт или заблокирован либо не может быть выделен здесь
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one объект {value} не является сохранённым контуром: он изменился или это другой документ
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` должен состоять из "v1:" и 16 шестнадцатеричных цифр в нижнем регистре
|
||||
Select Object… Выделить объект…
|
||||
Select Object Выделить объект
|
||||
Set Selection Задать выделение
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2215,6 +2215,8 @@ font-technology Variable Variabla
|
||||
Normal Screen Mode Normalt skärmläge
|
||||
Note: Obs:
|
||||
Nothing matches the Show filter. Inget matchar filtret Visa.
|
||||
Show DCS Transparency Interactions Visa DCS-genomskinlighetsinteraktioner
|
||||
Transparent, or overlaps transparent objects: check its separations Genomskinlig eller överlappar genomskinliga objekt: kontrollera dess separationer
|
||||
Nothing to print Inget att skriva ut
|
||||
Nudge Flytta stegvis
|
||||
Nudge in Perspective Flytta stegvis i perspektiv
|
||||
@@ -5347,6 +5349,65 @@ lighting Light Ljus
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} kan inte skapa en solid av en bild: kalkera den först med Objekt › Bildkalkering › Skapa.
|
||||
@msg {label} requires a shape with at least two anchors {label} kräver en form med minst två ankarpunkter
|
||||
@msg The shape is too complex for a 3D effect Formen är för komplex för en 3D-effekt
|
||||
@msg Extrude & Bevel Extrudera och fasa
|
||||
@msg Revolve Rotera runt axel
|
||||
@msg Inflate Blås upp
|
||||
@msg Rotate Rotera
|
||||
@msg Extrude & Bevel (Classic) Extrudera och fasa (klassisk)
|
||||
@msg Revolve (Classic) Rotera runt axel (klassisk)
|
||||
@msg Rotate (Classic) Rotera (klassisk)
|
||||
@msg This effect Den här effekten
|
||||
@msg clipping paths urklippsbanor
|
||||
@msg other objects andra objekt
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label} kräver en grupp med en form med minst två ankarpunkter
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Rotera runt axel kräver en grupp med en form med minst två ankarpunkter
|
||||
@msg Revolve requires a shape with at least two anchors Rotera runt axel kräver en form med minst två ankarpunkter
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} på en grupp kräver banor, sammansatta banor, text, grupper, urklippsgrupper, blandningar och symbolinstanser: frigör eller expandera först följande: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} på en grupp tar högst {MAX_MEMBERS} objekt
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke En 3D-effekt på en grupp eller ett lager hör till själva gruppen, inte till dess fyllning eller linje
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing en 3D-effekt på en grupps fyllning eller linje ritar ingenting
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export en 3D-effekt på en grupps fyllning eller linje ritar ingenting att exportera
|
||||
@msg The 3D object is too complex to export 3D-objektet är för komplext för att exporteras
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) objektet har ingen 3D-effekt (Extrudera och fasa, Rotera runt axel, Blås upp eller Rotera)
|
||||
@msg its 3D effect builds no solid dess 3D-effekt bygger ingen solid
|
||||
@msg bevel must be none or one of {_1} bevel måste vara none eller något av {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} kan inte bygga solider av den här teckningen: dess symbolinstanser, blandningar och urklippsgrupper innehåller för många objekt
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label} kan inte bygga en solid av instansen av symbolen ”{symbol}”: symbolens teckning är inte tillgänglig
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: urklippsbanan är för komplex för att klippa teckningen med
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} tar högst {MAX_MEMBERS} objekt
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1} kan inte bygga solider av teckning som är kapslad mer än {MAX_DEPTH} nivåer djupt
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient Färgen för Rotera runt axel är för komplex för den här ytan; minska Segment eller förenkla övertoningen
|
||||
@msg Revolve edge must be left or right Rotera runt axel: edge måste vara left eller right
|
||||
@msg Revolve cap must be a boolean Rotera runt axel: cap måste vara ett booleskt värde
|
||||
@msg the revolved object has no fill or stroke det roterade objektet har ingen fyllning eller linje
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor måste vara en ogenomskinlig RGB-färg i formen #rrggbb
|
||||
@msg lights must contain one to ten lights lights måste innehålla ett till tio ljus
|
||||
@msg each light must be an object varje ljus måste vara ett objekt
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind okänd ljusinställning: använd color, intensity, rotation, height, softness eller behind
|
||||
@msg light {key} must be a number from {min} to {max} ljus: {key} måste vara ett tal från {min} till {max}
|
||||
@msg light color must use opaque #rrggbb color för ett ljus måste vara en ogenomskinlig #rrggbb
|
||||
@msg light behind must be true or false behind för ett ljus måste vara true eller false
|
||||
@msg shadow must be true or false shadow måste vara true eller false
|
||||
@msg shadowPosition must be behind or below shadowPosition måste vara behind eller below
|
||||
@msg {key} must be a number from {min} to {max} {key} måste vara ett tal från {min} till {max}
|
||||
@msg each of `{GRAPHICS}` must be an object varje post i `{GRAPHICS}` måste vara ett objekt
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` posterna i `{GRAPHICS}` tar symbol, face, x, y, scale och rotation, inte `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol varje post i `{GRAPHICS}` behöver `symbol`, namnet på en symbol
|
||||
@msg `{GRAPHICS}` face must be front or back face i `{GRAPHICS}` måste vara front eller back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} {key} i `{GRAPHICS}` måste vara ett tal från {lo} till {hi}
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` måste vara en matris
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Grafik placeras på plana sidor: använd objekttypen Extrudera eller Plan
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` tar högst {MAX_GRAPHICS} grafiker
|
||||
@msg A plane has one face: map its graphics onto the front Ett plan har en sida: placera dess grafik på framsidan
|
||||
@msg The graphics are too complex: simplify the symbols' art Grafiken är för komplex: förenkla symbolernas teckning
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art En grafiks teckning får vara kapslad högst {MAX_LEVELS} nivåer djupt: förenkla symbolens teckning
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art En grafiks teckning får kapsla symboler högst {MAX_NESTING} nivåer djupt: förenkla symbolens teckning
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art En grafiks teckning innehåller för många objekt: förenkla symbolens teckning
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art En grafik ritar högst {MAX_PIECES} fyllningar och linjer: förenkla symbolens teckning
|
||||
@msg `surface` must be one of {_1} `surface` måste vara något av {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} ritar källteckningen för det här objektet i stället för dess 3D-yta
|
||||
@msg {label} draws no faces for this object {label} ritar inga sidor för det här objektet
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges trådramen har fler än {MAX_WIRE_EDGES} kanter
|
||||
@msg The 3D surface is too complex; simplify the shape 3D-ytan är för komplex; förenkla formen
|
||||
Transform Objects Transformera objekt
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? okänd parameter `{key}`; menade du `{near}`?
|
||||
@@ -6442,6 +6503,13 @@ axis Y Axis Y-axel
|
||||
@msg Batch cannot play an action with a stop Batch kan inte spela upp en åtgärd med ett stopp
|
||||
Insert Menu Item… Infoga menyalternativ…
|
||||
Insert Menu Item Infoga menyalternativ
|
||||
Insert Select Path Infoga markering av bana
|
||||
Select Path Markera bana
|
||||
@msg select one whole path or compound path markera en hel bana eller sammansatt bana
|
||||
@msg select only one path markera bara en bana
|
||||
@msg the path is hidden or locked, or cannot be selected here banan är dold eller låst, eller kan inte markeras här
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one objekt {value} är inte den sparade banan: den har ändrats, eller så är dokumentet ett annat
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` måste vara "v1:" och 16 hexadecimala siffror med gemener
|
||||
Select Object… Markera objekt…
|
||||
Select Object Markera objekt
|
||||
Set Selection Ange markering
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1896,6 +1896,142 @@ fn plugin_error_reasons_are_translated_with_the_error() {
|
||||
assert!(message(de(), &errors[3].0).ends_with("objects[0]: `opacity` muss eine Zahl sein"), "{}", message(de(), &errors[3].0));
|
||||
}
|
||||
|
||||
/// The 3D effects' source files under `crates/effects/src`. Their refusals reach the status bar
|
||||
/// through the effect commands, and every `format!` text in them outside tests is a refusal.
|
||||
const THREE_D_SOURCES: &[&str] = &[
|
||||
"solids.rs",
|
||||
"solids/classic.rs",
|
||||
"solids/graphics.rs",
|
||||
"solids/shadow.rs",
|
||||
"solids/source.rs",
|
||||
"solids/wireframe.rs",
|
||||
"revolve.rs",
|
||||
"revolve/classic.rs",
|
||||
"lighting.rs",
|
||||
];
|
||||
|
||||
/// The values 3D refusals put in their templates that are messages of their own: the name of an
|
||||
/// effect that has no 3D type of its own, and the objects a group's 3D effect can't build from.
|
||||
const THREE_D_PARTS: &[&str] = &["This effect", "clipping paths", "other objects"];
|
||||
|
||||
/// The 3D effects' refusals as catalog templates: the messages of [`THREE_D_SOURCES`] that
|
||||
/// [`source_messages`] finds, and the `format!` texts their helpers return.
|
||||
fn three_d_refusals() -> std::collections::BTreeSet<String> {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../effects/src");
|
||||
let texts: Vec<String> = THREE_D_SOURCES.iter().map(|f| std::fs::read_to_string(dir.join(f)).expect("a 3D source file")).collect();
|
||||
let mut consts = std::collections::BTreeMap::new();
|
||||
for text in &texts {
|
||||
for (name, value) in str_consts(without_test_module(text)) {
|
||||
consts.entry(name).or_insert_with(Vec::new).push(value);
|
||||
}
|
||||
}
|
||||
texts.iter().flat_map(|text| source_messages(without_test_module(text), &[("format!(", 0)], &consts)).collect()
|
||||
}
|
||||
|
||||
/// The names 3D refusals give their effect (`{label}`): each 3D effect's menu label without its
|
||||
/// ellipsis, and [`THREE_D_PARTS`].
|
||||
fn three_d_parts() -> Vec<String> {
|
||||
let labels = vectorcraft_effects::effect_catalog().into_iter().filter(|e| vectorcraft_effects::is_three_d(e.id));
|
||||
labels.map(|e| e.label.trim_end_matches('…').to_string()).chain(THREE_D_PARTS.iter().map(|p| p.to_string())).collect()
|
||||
}
|
||||
|
||||
/// The 3D effects' refusals, and the effect names and objects they name, have an `@msg` row in
|
||||
/// every complete-message catalog.
|
||||
#[test]
|
||||
fn three_d_refusals_are_translated_in_every_complete_catalog() {
|
||||
let refusals = three_d_refusals();
|
||||
assert!(refusals.len() > 55, "scan found only {} 3D refusals", refusals.len());
|
||||
let helper_made = [
|
||||
"{label} can't build a solid from an image: trace it first with Object › Image Trace › Make.",
|
||||
"{_1} can't build solids from art nested more than {MAX_DEPTH} levels deep",
|
||||
"the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate)",
|
||||
];
|
||||
for m in helper_made {
|
||||
assert!(refusals.contains(m), "the scan reads helpers' texts and constants: {m}");
|
||||
}
|
||||
let parts = three_d_parts();
|
||||
for label in ["Extrude & Bevel", "Revolve", "Inflate", "Rotate", "Extrude & Bevel (Classic)", "Revolve (Classic)", "Rotate (Classic)"] {
|
||||
assert!(parts.iter().any(|p| p == label), "{label} is a 3D effect's name");
|
||||
}
|
||||
let mut missing = vec![];
|
||||
for code in COMPLETE_MESSAGES {
|
||||
let lang = Lang::from_code(code).expect("registered");
|
||||
let (entries, _) = parse_entries(lang.0.source);
|
||||
let rows: std::collections::BTreeSet<&str> = entries.iter().filter(|(ctx, _, _)| ctx == "@msg").map(|(_, src, _)| src.as_str()).collect();
|
||||
missing.extend(refusals.iter().chain(&parts).filter(|m| !rows.contains(m.as_str())).map(|m| format!("{code}: {m}")));
|
||||
}
|
||||
assert!(missing.is_empty(), "{} 3D messages have no @msg row:\n{}", missing.len(), missing.join("\n"));
|
||||
}
|
||||
|
||||
/// A 3D effect's refusal shows in the interface language whole, with the effect's translated
|
||||
/// name, and with its numbers, the parameter keys it names and a symbol's name as given; the
|
||||
/// error the command returns stays English.
|
||||
#[test]
|
||||
fn three_d_refusals_translate_with_their_values() {
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use vectorcraft_doc::{Node, NodeId, NodeKind};
|
||||
use vectorcraft_effects::{CLASSIC_EXTRUDE, EXTRUDE};
|
||||
let mut s = vectorcraft_engine::Session::new();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
let id = s.execute("path.create", &json!({"d": "M10 10L60 10L60 60L10 60Z"})).unwrap()["id"].as_u64().unwrap();
|
||||
let rect = s.active().unwrap().doc.node(NodeId(id)).unwrap().clone();
|
||||
let group = Node::group(NodeId(id + 1), vec![Arc::new(rect.clone())]);
|
||||
let instance = Node::new(NodeId(id + 2), NodeKind::SymbolInstance { symbol: "Mine 7".into(), xf: vectorcraft_geom::Affine::IDENTITY });
|
||||
let refuse = |r: Result<(), String>| {
|
||||
let e = r.expect_err("refused");
|
||||
vectorcraft_engine::EngineError::BadParams { cmd: "effect.apply".into(), msg: e }.to_string()
|
||||
};
|
||||
let errors = [
|
||||
(refuse(vectorcraft_effects::validate_three_d_at(&group, EXTRUDE, &json!({}), Some((0, 0)))), "goes on the group itself", vec![]),
|
||||
(
|
||||
refuse(vectorcraft_effects::validate_three_d(&rect, EXTRUDE, &json!({"lights": [{"intensity": 500}]}))),
|
||||
"must be a number from",
|
||||
vec!["0", "100"],
|
||||
),
|
||||
(
|
||||
refuse(vectorcraft_effects::validate_three_d(&rect, EXTRUDE, &json!({"graphics": [{"symbol": "S", "face": "side"}]}))),
|
||||
"must be front or back",
|
||||
vec!["`graphics`", "front", "back"],
|
||||
),
|
||||
(refuse(vectorcraft_effects::validate_three_d(&rect, CLASSIC_EXTRUDE, &json!({"depth": "deep"}))), "must be a number", vec!["`depth`"]),
|
||||
(refuse(vectorcraft_effects::validate_three_d(&instance, EXTRUDE, &json!({}))), "the symbol's art isn't available", vec!["Mine 7"]),
|
||||
(refuse(vectorcraft_effects::three_d_wireframe_in(None, &rect).map(drop)), "has no 3D effect", vec![]),
|
||||
];
|
||||
for (e, english, _) in &errors {
|
||||
assert!(e.starts_with("invalid parameters for `effect.apply`: ") && e.contains(english), "{e}");
|
||||
assert_eq!(message(Lang::EN, e), *e);
|
||||
}
|
||||
assert!(errors[1].0.contains("light intensity must be a number from 0 to 100"), "{}", errors[1].0);
|
||||
assert!(errors[4].0.contains("Extrude & Bevel can't build a solid from the instance of symbol \"Mine 7\""), "{}", errors[4].0);
|
||||
for code in COMPLETE_MESSAGES {
|
||||
let lang = Lang::from_code(code).expect("registered");
|
||||
for (e, english, kept) in &errors {
|
||||
let shown = message(lang, e);
|
||||
assert!(!shown.contains(english) && shown.contains("`effect.apply`"), "{code}: {e} -> {shown}");
|
||||
assert!(kept.iter().all(|k| shown.contains(k)), "{code}: {e} -> {shown}");
|
||||
}
|
||||
let symbol = message(lang, &errors[4].0);
|
||||
let label = message(lang, "Extrude & Bevel");
|
||||
assert!(label != "Extrude & Bevel" && symbol.contains(&label), "{code}: the effect's name is translated: {symbol}");
|
||||
}
|
||||
assert_eq!(message(de(), &errors[1].0), "ungültige Parameter für `effect.apply`: Licht: intensity muss eine Zahl von 0 bis 100 sein");
|
||||
}
|
||||
|
||||
/// A value a message quotes is a name and is shown as written, even when a catalog row translates
|
||||
/// the same text: a symbol called Rotate stays Rotate while the effect's name is translated.
|
||||
#[test]
|
||||
fn quoted_values_in_a_message_stay_as_written() {
|
||||
let s = "Extrude & Bevel can't build a solid from the instance of symbol \"Rotate\": the symbol's art isn't available";
|
||||
for code in COMPLETE_MESSAGES {
|
||||
let lang = Lang::from_code(code).expect("registered");
|
||||
let shown = message(lang, s);
|
||||
let rotate = message(lang, "Rotate");
|
||||
assert!(rotate != "Rotate" && shown.contains("Rotate") && !shown.contains(&rotate), "{code}: {shown}");
|
||||
assert!(shown.contains(&message(lang, "Extrude & Bevel")), "{code}: {shown}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The Gamut Check status line names a count of 1 in the singular and other counts in the plural,
|
||||
/// and every complete catalog translates it, in the form its plural rule picks.
|
||||
#[test]
|
||||
@@ -3262,6 +3398,7 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[
|
||||
("axis must be \"vertical\", \"horizontal\" or an angle in degrees, not `{v}`", &["axis"]),
|
||||
("balance must be between 0 and 100", &["balance"]),
|
||||
("before must name a panel", &["before"]),
|
||||
("bevel must be none or one of {_1}", &["bevel", "none"]),
|
||||
("brightness must be dark|mediumDark|mediumLight|light", &["brightness", "dark", "mediumDark", "mediumLight", "light"]),
|
||||
("clip must be true or false", &["clip"]),
|
||||
("collapsed must be true or false", &["collapsed"]),
|
||||
@@ -3281,6 +3418,10 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[
|
||||
("kerning must be a number, \"auto\", or \"optical\"", &["kerning"]),
|
||||
("lang must be one of {_1}", &["lang"]),
|
||||
("layers must be true or false", &["layers"]),
|
||||
("light behind must be true or false", &["behind"]),
|
||||
("light color must use opaque #rrggbb", &["color"]),
|
||||
("lightColor must be an opaque RGB color in #rrggbb form", &["lightColor"]),
|
||||
("lights must contain one to ten lights", &["lights"]),
|
||||
("member is for order front, forward, backward or back", &["member", "order", "front", "forward", "backward", "back"]),
|
||||
("mode is set, add or toggle, not `{other}`", &["mode", "set", "add", "toggle"]),
|
||||
("names is empty", &["names"]),
|
||||
@@ -3295,18 +3436,28 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[
|
||||
("plane must be left, right or ground", &["plane", "left", "right", "ground"]),
|
||||
("precision must be from 0.001 to 10 pt", &["precision"]),
|
||||
("rect is [x, y, width, height] in points", &["rect"]),
|
||||
("Revolve cap must be a boolean", &["cap"]),
|
||||
("Revolve edge must be left or right", &["edge", "left", "right"]),
|
||||
("scale must be a number from {_1} to {_2}, not {_3}", &["scale"]),
|
||||
("scale must be a number from {_1} to {_2}, not {s}", &["scale"]),
|
||||
("shadow must be true or false", &["shadow"]),
|
||||
("shadowPosition must be behind or below", &["shadowPosition", "behind", "below"]),
|
||||
("show must be true or false", &["show"]),
|
||||
("span: \"selection\" places a linear, radial, conic or diamond gradient, not a freeform one", &["span"]),
|
||||
("thickness must be from 0.01 to 5000 pt", &["thickness"]),
|
||||
("title can't be empty", &["title"]),
|
||||
(
|
||||
"unknown light setting: use color, intensity, rotation, height, softness or behind",
|
||||
&["color", "intensity", "rotation", "height", "softness", "behind"],
|
||||
),
|
||||
("view is required", &["view"]),
|
||||
("width must be positive", &["width"]),
|
||||
("zone must be a string", &["zone"]),
|
||||
("zone must be tab, center, left, right, top or bottom", &["zone", "tab", "center", "left", "right", "top", "bottom"]),
|
||||
("`dither` applies to gradient fills (stroke: false)", &["stroke"]),
|
||||
("`strokeMode` applies to strokes (stroke: true)", &["stroke"]),
|
||||
("`{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}`", &["symbol", "face", "x", "y", "scale", "rotation"]),
|
||||
("`{GRAPHICS}` face must be front or back", &["face", "front", "back"]),
|
||||
];
|
||||
|
||||
/// Every catalog's translation of a [`NAMED_KEYS`] message keeps each name the table lists, as
|
||||
|
||||
@@ -2085,6 +2085,8 @@ font-technology Variable Değişken
|
||||
Normal Screen Mode Normal Ekran Modu
|
||||
Note: Not:
|
||||
Nothing matches the Show filter. Hiçbir şey Göster filtresiyle eşleşmiyor.
|
||||
Show DCS Transparency Interactions DCS Saydamlık Etkileşimlerini Göster
|
||||
Transparent, or overlaps transparent objects: check its separations Saydam veya saydam nesnelerle çakışıyor: ayrımlarını denetleyin
|
||||
Nothing to print Yazdırılacak bir şey yok
|
||||
Nudge Dürt
|
||||
Nudge in Perspective Perspektifte İtme
|
||||
@@ -5142,6 +5144,65 @@ lighting Light Işık
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label} bir görüntüden katı cisim oluşturamaz: önce Nesne › Görüntü İzleme › Yap ile izleyin.
|
||||
@msg {label} requires a shape with at least two anchors {label} en az iki bağlantı noktasına sahip bir şekil gerektirir
|
||||
@msg The shape is too complex for a 3D effect Şekli 3 boyutlu bir efekt için fazla karmaşık
|
||||
@msg Extrude & Bevel Ekstrüzyon ve Eğim
|
||||
@msg Revolve Döndür
|
||||
@msg Inflate Şişir
|
||||
@msg Rotate Döndür
|
||||
@msg Extrude & Bevel (Classic) Ekstrüzyon ve Eğim (Klasik)
|
||||
@msg Revolve (Classic) Döndür (Klasik)
|
||||
@msg Rotate (Classic) Çevir (Klasik)
|
||||
@msg This effect Bu efekt
|
||||
@msg clipping paths kırpma yolları
|
||||
@msg other objects diğer nesneler
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label}, en az iki bağlantı noktasına sahip bir şekil içeren bir grup gerektirir
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Döndür, en az iki bağlantı noktasına sahip bir şekil içeren bir grup gerektirir
|
||||
@msg Revolve requires a shape with at least two anchors Döndür, en az iki bağlantı noktasına sahip bir şekil gerektirir
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. Bir gruptaki {label}; yollar, bileşik yollar, metin, gruplar, kırpma grupları, karışımlar ve sembol örnekleri gerektirir: önce şunları serbest bırak veya genişlet: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects Bir gruptaki {label} en fazla {MAX_MEMBERS} nesne alır
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke Bir grup veya katmandaki 3 boyutlu efekt, dolgusuna veya konturuna değil, grubun kendisine uygulanır
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing bir grubun dolgusundaki veya konturundaki 3 boyutlu efekt hiçbir şey çizmez
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export bir grubun dolgusundaki veya konturundaki 3 boyutlu efekt dışa aktarılacak hiçbir şey çizmez
|
||||
@msg The 3D object is too complex to export 3 boyutlu nesne dışa aktarmak için çok karmaşık
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) nesnenin 3 boyutlu efekti yok (Ekstrüzyon ve Eğim, Döndür, Şişir veya Döndür)
|
||||
@msg its 3D effect builds no solid 3 boyutlu efekti hiçbir katı oluşturmuyor
|
||||
@msg bevel must be none or one of {_1} bevel, none veya {_1} değerlerinden biri olmalıdır
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} bu resimden katı oluşturamaz: sembol örnekleri, karışımları ve kırpma grupları çok fazla nesne içeriyor
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label}, “{symbol}” sembolünün örneğinden katı oluşturamaz: sembolün resmi kullanılamıyor
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: kırpma yolu resmi kesmek için çok karmaşık
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1} en fazla {MAX_MEMBERS} nesne alır
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1}, {MAX_DEPTH} düzeyden daha derin iç içe geçmiş resimden katı oluşturamaz
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient Döndür boyası bu yüzey için çok karmaşık; Segmentler değerini azalt veya degradeyi basitleştir
|
||||
@msg Revolve edge must be left or right Döndür: edge, left veya right olmalıdır
|
||||
@msg Revolve cap must be a boolean Döndür: cap bir boole değeri olmalıdır
|
||||
@msg the revolved object has no fill or stroke döndürülen nesnenin dolgusu veya konturu yok
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor, #rrggbb biçiminde opak bir RGB rengi olmalıdır
|
||||
@msg lights must contain one to ten lights lights bir ile on arasında ışık içermelidir
|
||||
@msg each light must be an object her ışık bir nesne olmalıdır
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind bilinmeyen ışık ayarı: color, intensity, rotation, height, softness veya behind kullan
|
||||
@msg light {key} must be a number from {min} to {max} ışık: {key} değeri {min} ile {max} arasında bir sayı olmalıdır
|
||||
@msg light color must use opaque #rrggbb ışığın color değeri opak #rrggbb olmalıdır
|
||||
@msg light behind must be true or false ışığın behind değeri true veya false olmalıdır
|
||||
@msg shadow must be true or false shadow, true veya false olmalıdır
|
||||
@msg shadowPosition must be behind or below shadowPosition, behind veya below olmalıdır
|
||||
@msg {key} must be a number from {min} to {max} {key} değeri {min} ile {max} arasında bir sayı olmalıdır
|
||||
@msg each of `{GRAPHICS}` must be an object `{GRAPHICS}` içindeki her öğe bir nesne olmalıdır
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` `{GRAPHICS}` girdileri symbol, face, x, y, scale ve rotation alır, `{k}` almaz
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol `{GRAPHICS}` içindeki her öğe, bir sembolün adı olan `symbol` gerektirir
|
||||
@msg `{GRAPHICS}` face must be front or back `{GRAPHICS}` face değeri front veya back olmalıdır
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} `{GRAPHICS}` {key} değeri {lo} ile {hi} arasında bir sayı olmalıdır
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` bir dizi olmalıdır
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Grafikler düz yüzlere eşlenir: Ekstrüzyon veya Düzlem nesne türünü kullan
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` en fazla {MAX_GRAPHICS} grafik alır
|
||||
@msg A plane has one face: map its graphics onto the front Bir düzlemin tek yüzü vardır: grafiklerini ön yüze eşle
|
||||
@msg The graphics are too complex: simplify the symbols' art Grafikler çok karmaşık: sembollerin resmini basitleştir
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art Bir grafiğin resmi en fazla {MAX_LEVELS} düzey iç içe olabilir: sembolün resmini basitleştir
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art Bir grafiğin resmi sembolleri en fazla {MAX_NESTING} düzey iç içe yerleştirebilir: sembolün resmini basitleştir
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art Bir grafiğin resmi çok fazla nesne içeriyor: sembolün resmini basitleştir
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Bir grafik en fazla {MAX_PIECES} dolgu ve kontur çizer: sembolün resmini basitleştir
|
||||
@msg `surface` must be one of {_1} `surface`, {_1} değerlerinden biri olmalıdır
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label} bu nesne için 3 boyutlu yüzeyi yerine kaynak resmi çizer
|
||||
@msg {label} draws no faces for this object {label} bu nesne için hiçbir yüz çizmez
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges tel kafeste {MAX_WIRE_EDGES} kenardan fazla var
|
||||
@msg The 3D surface is too complex; simplify the shape 3 boyutlu yüzey çok karmaşık; şekli basitleştir
|
||||
Transform Objects Nesneleri Dönüştür
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? bilinmeyen parametre `{key}`; `{near}` mi demek istediniz?
|
||||
@@ -6417,6 +6478,13 @@ axis Y Axis Y ekseni
|
||||
@msg Batch cannot play an action with a stop Toplu işlem durak içeren bir eylemi oynatamaz
|
||||
Insert Menu Item… Menü Öğesi Ekle…
|
||||
Insert Menu Item Menü Öğesi Ekle
|
||||
Insert Select Path Yol Seçimi Ekle
|
||||
Select Path Yolu Seç
|
||||
@msg select one whole path or compound path bir yolun tamamını veya bir bileşik yolu seçin
|
||||
@msg select only one path yalnızca bir yol seçin
|
||||
@msg the path is hidden or locked, or cannot be selected here yol gizli veya kilitli ya da burada seçilemiyor
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one {value} nesnesi kaydedilen yol değil: değişti ya da belge başka
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` "v1:" ve 16 küçük harfli onaltılık basamak olmalıdır
|
||||
Select Object… Nesne Seç…
|
||||
Select Object Nesne Seç
|
||||
Set Selection Seçimi Ayarla
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -2306,6 +2306,8 @@ file-filter Templates Шаблони
|
||||
Normal Screen Mode Звичайний режим екрану
|
||||
Note: Примітка:
|
||||
Nothing matches the Show filter. Нічого не відповідає фільтру "Показати".
|
||||
Show DCS Transparency Interactions Показувати взаємодії DCS із прозорістю
|
||||
Transparent, or overlaps transparent objects: check its separations Прозоре або перекриває прозорі об'єкти: перевірте кольороподіл
|
||||
Nothing to print Нема чого друкувати
|
||||
Nudge Зрушити
|
||||
Nudge in Perspective Зрушити у перспективі
|
||||
@@ -5473,6 +5475,65 @@ lighting Light Світло
|
||||
@msg {label} can't build a solid from an image: trace it first with Object › Image Trace › Make. {label}: із зображення не можна побудувати тіло. Спершу трасуйте його командою Об’єкт › Трасування зображення › Створити.
|
||||
@msg {label} requires a shape with at least two anchors {label}: потрібна фігура щонайменше з двома опорними точками
|
||||
@msg The shape is too complex for a 3D effect Фігура надто складна для 3D-ефекту
|
||||
@msg Extrude & Bevel Видавлювання та фаска
|
||||
@msg Revolve Обертання
|
||||
@msg Inflate Надування
|
||||
@msg Rotate Повернути
|
||||
@msg Extrude & Bevel (Classic) Видавлювання та фаска (класичне)
|
||||
@msg Revolve (Classic) Обертання (класичне)
|
||||
@msg Rotate (Classic) Поворот (класичний)
|
||||
@msg This effect Цей ефект
|
||||
@msg clipping paths відсічні контури
|
||||
@msg other objects інші об’єкти
|
||||
@msg {label} requires a group holding a shape with at least two anchors {label}: потрібна група з фігурою щонайменше з двома опорними точками
|
||||
@msg Revolve requires a group holding a shape with at least two anchors Обертання: потрібна група з фігурою щонайменше з двома опорними точками
|
||||
@msg Revolve requires a shape with at least two anchors Обертання: потрібна фігура щонайменше з двома опорними точками
|
||||
@msg {label} on a group needs paths, compound paths, type, groups, clipping groups, blends and symbol instances: release or expand its {what} first. {label} для групи: потрібні контури, складені контури, текст, групи, відсічні групи, переходи та екземпляри символів; спершу звільніть або розберіть ці елементи: {what}.
|
||||
@msg {label} on a group takes at most {MAX_MEMBERS} objects {label} для групи: допускається не більше {MAX_MEMBERS} об’єктів
|
||||
@msg A 3D effect on a group or layer goes on the group itself, not on its fill or stroke 3D-ефект для групи або шару застосовується до самої групи, а не до її заливки чи обведення
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing 3D-ефект на заливці або обведенні групи нічого не малює
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 3D-ефект на заливці або обведенні групи не малює нічого для експорту
|
||||
@msg The 3D object is too complex to export 3D-об’єкт надто складний для експорту
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) об’єкт не має 3D-ефекту (Видавлювання та фаска, Обертання, Надування або Повернути)
|
||||
@msg its 3D effect builds no solid його 3D-ефект не будує тіла
|
||||
@msg bevel must be none or one of {_1} bevel має бути none або одним з: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: не вдається побудувати тіла з цієї ілюстрації: її екземпляри символів, переходи та відсічні групи містять забагато об’єктів
|
||||
@msg {label} can't build a solid from the instance of symbol "{symbol}": the symbol's art isn't available {label}: не вдається побудувати тіло з екземпляра символу «{symbol}»: ілюстрація символу недоступна
|
||||
@msg {_1}: the clipping path is too complex to cut the art with {_1}: відсічний контур надто складний, щоб обрізати ним ілюстрацію
|
||||
@msg {_1} takes at most {MAX_MEMBERS} objects {_1}: допускається не більше {MAX_MEMBERS} об’єктів
|
||||
@msg {_1} can't build solids from art nested more than {MAX_DEPTH} levels deep {_1}: не вдається побудувати тіла з ілюстрації з вкладеністю понад {MAX_DEPTH} рівнів
|
||||
@msg Revolve paint is too complex for this surface; reduce Segments or simplify the gradient Фарба «Обертання» надто складна для цієї поверхні; зменште «Сегменти» або спростіть градієнт
|
||||
@msg Revolve edge must be left or right Обертання: edge має бути left або right
|
||||
@msg Revolve cap must be a boolean Обертання: cap має бути логічним значенням
|
||||
@msg the revolved object has no fill or stroke об’єкт обертання не має заливки чи обведення
|
||||
@msg lightColor must be an opaque RGB color in #rrggbb form lightColor має бути непрозорим кольором RGB у форматі #rrggbb
|
||||
@msg lights must contain one to ten lights lights має містити від одного до десяти джерел світла
|
||||
@msg each light must be an object кожне джерело світла має бути об’єктом
|
||||
@msg unknown light setting: use color, intensity, rotation, height, softness or behind невідомий параметр джерела світла: використайте color, intensity, rotation, height, softness або behind
|
||||
@msg light {key} must be a number from {min} to {max} джерело світла: {key} має бути числом від {min} до {max}
|
||||
@msg light color must use opaque #rrggbb color джерела світла має бути непрозорим #rrggbb
|
||||
@msg light behind must be true or false behind джерела світла має бути true або false
|
||||
@msg shadow must be true or false shadow має бути true або false
|
||||
@msg shadowPosition must be behind or below shadowPosition має бути behind або below
|
||||
@msg {key} must be a number from {min} to {max} {key} має бути числом від {min} до {max}
|
||||
@msg each of `{GRAPHICS}` must be an object кожен елемент `{GRAPHICS}` має бути об’єктом
|
||||
@msg `{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}` елементи `{GRAPHICS}` приймають symbol, face, x, y, scale і rotation, а не `{k}`
|
||||
@msg each of `{GRAPHICS}` needs `symbol`, the name of a symbol кожному елементу `{GRAPHICS}` потрібен `symbol`, назва символу
|
||||
@msg `{GRAPHICS}` face must be front or back face у `{GRAPHICS}` має бути front або back
|
||||
@msg `{GRAPHICS}` {key} must be a number from {lo} to {hi} {key} у `{GRAPHICS}` має бути числом від {lo} до {hi}
|
||||
@msg `{GRAPHICS}` must be an array `{GRAPHICS}` має бути масивом
|
||||
@msg Graphics map onto planar faces: use the Extrude or Plane object type Графіка наноситься на плоскі грані: використайте тип об’єкта «Видавлювання» або «Площина»
|
||||
@msg `{GRAPHICS}` takes at most {MAX_GRAPHICS} graphics `{GRAPHICS}` приймає не більше {MAX_GRAPHICS} графічних елементів
|
||||
@msg A plane has one face: map its graphics onto the front Площина має одну сторону: розміщуйте її графіку на передній стороні
|
||||
@msg The graphics are too complex: simplify the symbols' art Графіка надто складна: спростіть ілюстрації символів
|
||||
@msg A graphic's art may be nested at most {MAX_LEVELS} levels deep: simplify the symbol's art Ілюстрація графіки може бути вкладена не більше ніж на {MAX_LEVELS} рівнів: спростіть ілюстрацію символу
|
||||
@msg A graphic's art may nest symbols at most {MAX_NESTING} levels deep: simplify the symbol's art Ілюстрація графіки може вкладати символи не більше ніж на {MAX_NESTING} рівнів: спростіть ілюстрацію символу
|
||||
@msg A graphic's art holds too many objects: simplify the symbol's art Ілюстрація графіки містить забагато об’єктів: спростіть ілюстрацію символу
|
||||
@msg A graphic draws at most {MAX_PIECES} fills and strokes: simplify the symbol's art Графіка малює не більше {MAX_PIECES} заливок і обведень: спростіть ілюстрацію символу
|
||||
@msg `surface` must be one of {_1} `surface` має бути одним з: {_1}
|
||||
@msg {label} draws the source art for this object instead of its 3D surface {label}: для цього об’єкта малюється вихідна ілюстрація замість його 3D-поверхні
|
||||
@msg {label} draws no faces for this object {label}: для цього об’єкта не малюється жодної грані
|
||||
@msg the wireframe has more than {MAX_WIRE_EDGES} edges каркас має понад {MAX_WIRE_EDGES} ребер
|
||||
@msg The 3D surface is too complex; simplify the shape 3D-поверхня надто складна; спростіть фігуру
|
||||
Transform Objects Трансформувати об’єкти
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? невідомий параметр `{key}`; можливо, ви мали на увазі `{near}`?
|
||||
@@ -6066,6 +6127,13 @@ axis Y Axis Вісь Y
|
||||
@msg Batch cannot play an action with a stop Пакет не може відтворити дію із зупинкою
|
||||
Insert Menu Item… Вставити пункт меню…
|
||||
Insert Menu Item Вставити пункт меню
|
||||
Insert Select Path Вставити виділення контуру
|
||||
Select Path Виділити контур
|
||||
@msg select one whole path or compound path виділіть один цілий контур або складений контур
|
||||
@msg select only one path виділіть лише один контур
|
||||
@msg the path is hidden or locked, or cannot be selected here контур прихований або заблокований, або його не можна виділити тут
|
||||
@msg object {value} is not the stored path: it changed, or the document is another one об'єкт {value} не є збереженим контуром: його змінено або це інший документ
|
||||
@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` має складатися з "v1:" і 16 шістнадцяткових цифр у нижньому регістрі
|
||||
Select Object… Виділити об’єкт…
|
||||
Select Object Виділити об’єкт
|
||||
Set Selection Задати виділення
|
||||
|
||||
|
Can't render this file because it is too large.
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user