mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 21:44:57 +00:00
Merge pull request #7578 from dotCooCoo/fix/gradient-spot-colorants-drawn-stops
PDF and EPS export: gradients name only the spot inks of stops that color something
This commit is contained in:
@@ -547,6 +547,49 @@ impl Gradient {
|
||||
None => (Color::BLACK, 1.0),
|
||||
}
|
||||
}
|
||||
/// Which of its stops color some part of it that draws, by the stretches [`Self::sample`]
|
||||
/// mixes in, for stops in any order: the pair of stop `k` and stop `k + 1` covers the offsets
|
||||
/// above every offset of stops `0..=k` up to stop `k + 1`'s, and both its stops count when that
|
||||
/// stretch has positive width and either stop is above opacity 0. The first stop also counts
|
||||
/// when it is above opacity 0, since it colors the offsets up to its own, and so does the last
|
||||
/// stop, since it colors the offsets above every stop's. Any other stop, opaque or not, counts
|
||||
/// only through a pair: a stop out of order whose pairs have no width (another stop before it
|
||||
/// reaches past both ends) colors nothing. A freeform gradient draws from its points, so each of
|
||||
/// its stops counts while any stop is above opacity 0.
|
||||
pub fn drawn_stops(&self) -> Vec<bool> {
|
||||
self.stops_with_color(|s| s.opacity > 0.0)
|
||||
}
|
||||
/// [`Self::drawn_stops`] with every stop taken as opaque, for output that leaves stop opacity
|
||||
/// out: the stops that color a stretch of positive width, and the first and last stops.
|
||||
pub fn reached_stops(&self) -> Vec<bool> {
|
||||
self.stops_with_color(|_| true)
|
||||
}
|
||||
/// [`Self::drawn_stops`] with `shows` telling whether a stop's opacity shows its color.
|
||||
fn stops_with_color(&self, shows: impl Fn(&GradientStop) -> bool) -> Vec<bool> {
|
||||
let stops = &self.stops;
|
||||
if self.kind == GradientKind::Freeform {
|
||||
let any = stops.iter().any(&shows);
|
||||
return vec![any; stops.len()];
|
||||
}
|
||||
let mut drawn = vec![false; stops.len()];
|
||||
if let (Some(first), Some(d)) = (stops.first(), drawn.first_mut()) {
|
||||
*d = shows(first);
|
||||
}
|
||||
if let (Some(last), Some(d)) = (stops.last(), drawn.last_mut()) {
|
||||
*d |= shows(last);
|
||||
}
|
||||
// `f32::max` skips a NaN offset, which the sampler never picks either.
|
||||
let mut reach = f32::NEG_INFINITY;
|
||||
for k in 0..stops.len().saturating_sub(1) {
|
||||
reach = reach.max(stops[k].offset);
|
||||
let (a, b) = (&stops[k], &stops[k + 1]);
|
||||
if b.offset > reach && (shows(a) || shows(b)) {
|
||||
drawn[k] = true;
|
||||
drawn[k + 1] = true;
|
||||
}
|
||||
}
|
||||
drawn
|
||||
}
|
||||
/// [`Self::sample_with`] at each of `ts`, which must not decrease. Each search starts where
|
||||
/// the one before it stopped, so the work is the samples plus the stops, not their product.
|
||||
pub fn sample_increasing_with(&self, ts: impl IntoIterator<Item = f32>, mix: impl Fn(&Color, &Color, f32) -> Color) -> Vec<(Color, f32)> {
|
||||
|
||||
@@ -66,6 +66,69 @@ fn stop_reach_matches_scanning_the_stops() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient's stops color what draws where they color a stretch that draws, for stops in any
|
||||
/// order: the sampler mixes stop `k` with stop `k + 1` above every offset of the stops before it,
|
||||
/// so an out-of-order stop's pair can have no width.
|
||||
#[test]
|
||||
fn drawn_stops_follow_the_sampled_stretches() {
|
||||
let stop = |offset: f32, opacity: f32| GradientStop { opacity, ..GradientStop::new(offset, Color::BLACK) };
|
||||
let g = |stops: Vec<GradientStop>| Gradient::new(GradientKind::Linear, stops);
|
||||
// In order: the clear stop next to the opaque one, not the one after it.
|
||||
assert_eq!(g(vec![stop(0.0, 1.0), stop(0.5, 0.0), stop(1.0, 0.0)]).drawn_stops(), vec![true, true, false]);
|
||||
// Out of order: the pair (0.8, 0.3) has no width, so the clear stop at 0.3 mixes only with
|
||||
// the clear stop at 0.8 before it, and the pair (0.3, 1.0) is sampled above 0.8.
|
||||
assert_eq!(g(vec![stop(0.0, 0.0), stop(0.8, 0.0), stop(0.3, 0.0), stop(1.0, 1.0)]).drawn_stops(), vec![false, false, true, true]);
|
||||
// An opaque stop out of order: the pairs (0, 0.8) and (0.7, 1) cover every offset, so the
|
||||
// pairs next to the stop at 0.3 have no width and it colors nothing.
|
||||
let out_of_order = |o: f32| g(vec![stop(0.0, 0.0), stop(0.8, o), stop(0.3, 1.0), stop(0.7, 0.0), stop(1.0, 0.0)]);
|
||||
assert_eq!(out_of_order(0.0).drawn_stops(), vec![false; 5]);
|
||||
// With the stop at 0.8 opaque, the pairs next to it with width are drawn.
|
||||
assert_eq!(out_of_order(1.0).drawn_stops(), vec![true, true, false, false, false]);
|
||||
// The last stop colors the offsets above every stop's, even when it is out of order.
|
||||
assert_eq!(g(vec![stop(0.0, 0.0), stop(0.6, 0.0), stop(0.5, 1.0)]).drawn_stops(), vec![false, false, true]);
|
||||
// The first stop colors the offsets up to its own.
|
||||
assert_eq!(g(vec![stop(0.0, 1.0), stop(0.0, 0.0), stop(1.0, 0.0)]).drawn_stops(), vec![true, false, false]);
|
||||
// One stop, every stop clear, and no stops at all.
|
||||
assert_eq!(g(vec![stop(0.5, 1.0)]).drawn_stops(), vec![true]);
|
||||
assert_eq!(g(vec![stop(0.0, 0.0), stop(1.0, 0.0)]).drawn_stops(), vec![false, false]);
|
||||
assert!(g(vec![]).drawn_stops().is_empty());
|
||||
// A freeform gradient draws from its points: its stops count while any is above opacity 0.
|
||||
let mut free = g(vec![stop(0.0, 1.0), stop(0.5, 0.0), stop(1.0, 0.0)]);
|
||||
free.kind = GradientKind::Freeform;
|
||||
assert_eq!(free.drawn_stops(), vec![true, true, true]);
|
||||
free.stops.iter_mut().for_each(|s| s.opacity = 0.0);
|
||||
assert_eq!(free.drawn_stops(), vec![false; 3]);
|
||||
}
|
||||
|
||||
/// Taken as opaque, every stop that colors a stretch of positive width counts, and the first and
|
||||
/// last stops, whatever their opacity; an out-of-order stop whose pairs have no width does not.
|
||||
#[test]
|
||||
fn reached_stops_leave_out_only_stops_without_width() {
|
||||
let stop = |offset: f32, opacity: f32| GradientStop { opacity, ..GradientStop::new(offset, Color::BLACK) };
|
||||
let g = |stops: Vec<GradientStop>| Gradient::new(GradientKind::Linear, stops);
|
||||
// Clear stops in order color their stretches once opaque.
|
||||
assert_eq!(g(vec![stop(0.0, 1.0), stop(0.5, 0.0), stop(1.0, 0.0)]).reached_stops(), vec![true; 3]);
|
||||
assert_eq!(g(vec![stop(0.0, 0.0), stop(1.0, 0.0)]).reached_stops(), vec![true, true]);
|
||||
// The stop at 0.3 sits inside the stretch the stops at 0 and 0.8 cover.
|
||||
let out_of_order = g(vec![stop(0.0, 0.0), stop(0.8, 0.0), stop(0.3, 1.0), stop(0.7, 0.0), stop(1.0, 0.0)]);
|
||||
assert_eq!(out_of_order.reached_stops(), vec![true, true, false, true, true]);
|
||||
// A stop at the offset of the stop before it: its pair has no width, the next one does.
|
||||
assert_eq!(g(vec![stop(0.0, 1.0), stop(0.0, 1.0), stop(1.0, 1.0)]).reached_stops(), vec![true, true, true]);
|
||||
assert_eq!(
|
||||
g(vec![stop(0.0, 1.0), stop(0.6, 1.0), stop(0.6, 1.0), stop(0.2, 1.0), stop(1.0, 1.0)]).reached_stops(),
|
||||
vec![true, true, false, true, true]
|
||||
);
|
||||
assert!(g(vec![]).reached_stops().is_empty());
|
||||
// Every pair the sampler picks between the ends has both its stops counted.
|
||||
let stops: Vec<GradientStop> = [0.1, 0.4, 0.4, 0.2, 0.9, 0.5].iter().map(|&o| stop(o, 1.0)).collect();
|
||||
let g = g(stops);
|
||||
let (reached, reach) = (g.reached_stops(), StopReach::new(&g.stops));
|
||||
for t in (5..=95).map(|k| k as f32 / 100.0).filter(|&t| t > 0.1 && t <= 0.9) {
|
||||
let k = reach.0.partition_point(|r| *r < t);
|
||||
assert!(reached[k] && reached[k + 1], "pair {k} at {t}: {reached:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Repeated offsets turn off the direct midpoint mix, and the midpoints still come out as
|
||||
/// `sample` gives them, with work that stays near linear in the stops (thousands of stops at one
|
||||
/// offset used to scan them all for each midpoint).
|
||||
|
||||
@@ -344,44 +344,9 @@ fn spot_swatches(d: &Document) -> HashSet<&str> {
|
||||
first.into_iter().filter(|&(name, spot)| spot && name != vectorcraft_color::swatch::REGISTRATION).map(|(name, _)| name).collect()
|
||||
}
|
||||
|
||||
/// Which of gradient `g`'s stops color some part of it that draws, by the stretches
|
||||
/// [`vectorcraft_color::Gradient::sample`] mixes in, for stops in any order: the pair of stop `k`
|
||||
/// and stop `k + 1` covers the offsets above every offset of stops `0..=k` up to stop `k + 1`'s,
|
||||
/// and both its stops count when that stretch has positive width and either stop is above opacity
|
||||
/// 0. The first stop also counts when it is above opacity 0, since it colors the offsets up to its
|
||||
/// own, and so does the last stop, since it colors the offsets above every stop's. Any other stop,
|
||||
/// opaque or not, counts only through a pair: a stop out of order whose pairs have no width
|
||||
/// (another stop before it reaches past both ends) colors nothing. A freeform gradient draws from
|
||||
/// its points, so each of its stops counts while any stop is above opacity 0.
|
||||
fn drawn_stops(g: &vectorcraft_color::Gradient) -> Vec<bool> {
|
||||
let stops = &g.stops;
|
||||
if g.kind == vectorcraft_color::GradientKind::Freeform {
|
||||
let any = stops.iter().any(|s| s.opacity > 0.0);
|
||||
return vec![any; stops.len()];
|
||||
}
|
||||
let mut drawn = vec![false; stops.len()];
|
||||
if let (Some(first), Some(d)) = (stops.first(), drawn.first_mut()) {
|
||||
*d = first.opacity > 0.0;
|
||||
}
|
||||
if let (Some(last), Some(d)) = (stops.last(), drawn.last_mut()) {
|
||||
*d |= last.opacity > 0.0;
|
||||
}
|
||||
// `f32::max` skips a NaN offset, which the sampler never picks either.
|
||||
let mut reach = f32::NEG_INFINITY;
|
||||
for k in 0..stops.len().saturating_sub(1) {
|
||||
reach = reach.max(stops[k].offset);
|
||||
let (a, b) = (&stops[k], &stops[k + 1]);
|
||||
if b.offset > reach && (a.opacity > 0.0 || b.opacity > 0.0) {
|
||||
drawn[k] = true;
|
||||
drawn[k + 1] = true;
|
||||
}
|
||||
}
|
||||
drawn
|
||||
}
|
||||
|
||||
/// The spot colors paint `p` prints (`spots`: [`spot_swatches`]): the spot swatch a solid color is
|
||||
/// linked to, or those a gradient's drawn stops ([`drawn_stops`]) are linked to, each once, in the
|
||||
/// order of the stops. A link at a tint of 0 prints no ink, a stop that colors only parts of a
|
||||
/// linked to, or those a gradient's drawn stops ([`vectorcraft_color::Gradient::drawn_stops`]) are
|
||||
/// linked to, each once, in the order of the stops. A link at a tint of 0 prints no ink, a stop that colors only parts of a
|
||||
/// gradient at an opacity of 0 prints none, and a color that only looks like a spot color is not
|
||||
/// linked to one.
|
||||
fn spot_labels(spots: &HashSet<&str>, p: &Paint) -> Vec<String> {
|
||||
@@ -398,7 +363,7 @@ fn spot_labels(spots: &HashSet<&str>, p: &Paint) -> Vec<String> {
|
||||
match p {
|
||||
Paint::Solid { swatch, tint, .. } => add(swatch, *tint),
|
||||
Paint::Gradient(g) => {
|
||||
let drawn = drawn_stops(&g.gradient);
|
||||
let drawn = g.gradient.drawn_stops();
|
||||
g.gradient.stops.iter().zip(drawn).filter(|&(_, d)| d).for_each(|(s, _)| add(&s.swatch, s.tint));
|
||||
}
|
||||
_ => {}
|
||||
@@ -1662,39 +1627,6 @@ mod tests {
|
||||
assert!(!["Far", "Edge", "Seam"].iter().any(|n| text.contains(n)), "{text}");
|
||||
}
|
||||
|
||||
/// A gradient's stops print where they color a stretch that draws, for stops in any order: the
|
||||
/// sampler mixes stop `k` with stop `k + 1` above every offset of the stops before it, so an
|
||||
/// out-of-order stop's pair can have no width.
|
||||
#[test]
|
||||
fn drawn_stops_follow_the_sampled_stretches() {
|
||||
use vectorcraft_color::{Color, Gradient, GradientKind, GradientStop};
|
||||
let stop = |offset: f32, opacity: f32| GradientStop { opacity, ..GradientStop::new(offset, Color::BLACK) };
|
||||
let g = |stops: Vec<GradientStop>| Gradient::new(GradientKind::Linear, stops);
|
||||
// In order: the clear stop next to the opaque one, not the one after it.
|
||||
assert_eq!(drawn_stops(&g(vec![stop(0.0, 1.0), stop(0.5, 0.0), stop(1.0, 0.0)])), vec![true, true, false]);
|
||||
// Out of order: the pair (0.8, 0.3) has no width, so the clear stop at 0.3 mixes only with
|
||||
// the clear stop at 0.8 before it, and the pair (0.3, 1.0) is sampled above 0.8.
|
||||
assert_eq!(drawn_stops(&g(vec![stop(0.0, 0.0), stop(0.8, 0.0), stop(0.3, 0.0), stop(1.0, 1.0)])), vec![false, false, true, true]);
|
||||
// An opaque stop out of order: the pairs (0, 0.8) and (0.7, 1) cover every offset, so the
|
||||
// pairs next to the stop at 0.3 have no width and it colors nothing.
|
||||
let out_of_order = |o: f32| g(vec![stop(0.0, 0.0), stop(0.8, o), stop(0.3, 1.0), stop(0.7, 0.0), stop(1.0, 0.0)]);
|
||||
assert_eq!(drawn_stops(&out_of_order(0.0)), vec![false; 5]);
|
||||
// With the stop at 0.8 opaque, the pairs next to it with width are drawn.
|
||||
assert_eq!(drawn_stops(&out_of_order(1.0)), vec![true, true, false, false, false]);
|
||||
// The last stop colors the offsets above every stop's, even when it is out of order.
|
||||
assert_eq!(drawn_stops(&g(vec![stop(0.0, 0.0), stop(0.6, 0.0), stop(0.5, 1.0)])), vec![false, false, true]);
|
||||
// The first stop colors the offsets up to its own.
|
||||
assert_eq!(drawn_stops(&g(vec![stop(0.0, 1.0), stop(0.0, 0.0), stop(1.0, 0.0)])), vec![true, false, false]);
|
||||
// One stop, every stop clear, and no stops at all.
|
||||
assert_eq!(drawn_stops(&g(vec![stop(0.5, 1.0)])), vec![true]);
|
||||
assert_eq!(drawn_stops(&g(vec![stop(0.0, 0.0), stop(1.0, 0.0)])), vec![false, false]);
|
||||
assert!(drawn_stops(&g(vec![])).is_empty());
|
||||
// A freeform gradient draws from its points: its stops count while any is above opacity 0.
|
||||
let mut free = g(vec![stop(0.0, 1.0), stop(0.5, 0.0), stop(1.0, 0.0)]);
|
||||
free.kind = GradientKind::Freeform;
|
||||
assert_eq!(drawn_stops(&free), vec![true, true, true]);
|
||||
}
|
||||
|
||||
/// Brushes, Spot Color Objects, Pattern Objects and Gradient Objects leave out a hidden object,
|
||||
/// an object at an opacity of 0 and the objects inside a hidden layer or a group at an opacity of
|
||||
/// 0, with Selection Only too: their fills, strokes, characters and colorized pixels paint no
|
||||
|
||||
+14
-7
@@ -931,18 +931,21 @@ impl<'a> Scene<'a> {
|
||||
/// process inks the stops use, in CMYK order, and the spot inks, in the order the stops name
|
||||
/// them. Paper white adds no ink, a gray stop is black ink, and a spot stop's process plates
|
||||
/// print with its ink. An off-center midpoint is a stop
|
||||
/// with the inks halfway between its two stops. `None` for a gradient without a spot ink, and,
|
||||
/// with a warning, for one whose inks can't be kept or that is mixed by the Perceptual method
|
||||
/// or around the hue wheel; it is then written in process colors.
|
||||
/// with the inks halfway between its two stops. Stop opacity is left out, so the stops that
|
||||
/// color a stretch ([`vectorcraft_color::Gradient::reached_stops`]) name the inks, a spot ink
|
||||
/// only above a tint of 0 or with process plates; the others are written with no ink. `None` for a gradient without a
|
||||
/// spot ink, and, with a warning, for one whose inks can't be kept or that is mixed by the
|
||||
/// Perceptual method or around the hue wheel; it is then written in process colors.
|
||||
fn ink_stops(&mut self, g: &vectorcraft_color::Gradient) -> Option<(GradientSpace, RawStops)> {
|
||||
let doc = self.doc;
|
||||
let spot_of = |s: &vectorcraft_color::GradientStop| -> Option<String> {
|
||||
s.swatch.as_deref().filter(|n| doc.swatch(n).is_some_and(|w| w.spot)).map(str::to_string)
|
||||
};
|
||||
let reached = g.reached_stops();
|
||||
// The spot inks, collected up to one more than a DeviceN space can hold.
|
||||
let mut names: Vec<String> = vec![];
|
||||
for s in &g.stops {
|
||||
if let Some(n) = spot_of(s)
|
||||
for (s, &r) in g.stops.iter().zip(&reached) {
|
||||
if let Some(n) = spot_of(s).filter(|_| r && (s.tint > 0.0 || s.plates.is_some()))
|
||||
&& !names.contains(&n)
|
||||
{
|
||||
names.push(n);
|
||||
@@ -971,9 +974,10 @@ impl<'a> Scene<'a> {
|
||||
};
|
||||
// Each stop's ink coverage: the four process inks, then one tint per spot ink.
|
||||
let mut inks: Vec<Vec<f32>> = Vec::with_capacity(g.stops.len());
|
||||
for s in &g.stops {
|
||||
for (s, &r) in g.stops.iter().zip(&reached) {
|
||||
let mut v = vec![0.0; 4 + names.len()];
|
||||
match spot_of(s) {
|
||||
_ if !r => {}
|
||||
Some(n) => {
|
||||
if let Some(i) = names.iter().position(|m| *m == n) {
|
||||
v[4 + i] = unit(s.tint);
|
||||
@@ -1116,7 +1120,10 @@ impl<'a> Scene<'a> {
|
||||
// keep no swatch links: the warning [`Self::ink_stops`] gives the others.
|
||||
if g.gradient.method.is_hue() && g.gradient.is_spread() && g.gradient.spread == vectorcraft_color::GradientSpread::Reflect {
|
||||
let doc = self.doc;
|
||||
if g.gradient.stops.iter().any(|s| s.swatch.as_deref().is_some_and(|n| doc.swatch(n).is_some_and(|w| w.spot))) {
|
||||
let spot = |s: &vectorcraft_color::GradientStop| {
|
||||
(s.tint > 0.0 || s.plates.is_some()) && s.swatch.as_deref().is_some_and(|n| doc.swatch(n).is_some_and(|w| w.spot))
|
||||
};
|
||||
if g.gradient.stops.iter().zip(g.gradient.reached_stops()).any(|(s, r)| r && spot(s)) {
|
||||
self.warn(SPOT_GRADIENTS_HUE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,6 +532,66 @@ fn a_gradient_mixing_a_spot_ink_with_rgb_is_written_without_the_ink() {
|
||||
assert!(!out.warnings.iter().any(|w| w.contains("spot")), "{:?}", out.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradients_name_only_the_spot_inks_of_stops_that_color_a_stretch() {
|
||||
let black = |offset: f32| GradientStop::new(offset, Color::cmyk(0.0, 0.0, 0.0, 1.0));
|
||||
let paper = |offset: f32| GradientStop::new(offset, Color::cmyk(0.0, 0.0, 0.0, 0.0));
|
||||
let no_spot_space = |ps: &str| !ps.contains("(Gold Ink) cvn") && !ps.contains("/DeviceN") && !ps.contains("/Separation");
|
||||
for level in [Level::Two, Level::Three] {
|
||||
// Gold Ink out of order: the stops at 0 and 0.8 cover the stretches next to it, so it
|
||||
// colors nothing and has no plate.
|
||||
let d = spot_gradient_doc(vec![black(0.0), black(0.8), spot_stop(0.2, "Gold Ink", GOLD, 1.0), black(0.4), black(1.0)]);
|
||||
let out = export(&d, &EpsOptions { level, ..opts(&d) }, None, None).unwrap();
|
||||
let ps = text(&out);
|
||||
assert_eq!(dsc(&ps, "DocumentCustomColors"), None, "{level:?}: {ps}");
|
||||
assert!(no_spot_space(&ps), "{level:?}: {ps}");
|
||||
assert!(!out.warnings.iter().any(|w| w.contains("spot")), "{level:?}: {:?}", out.warnings);
|
||||
checked_page(&ps);
|
||||
// Without it, Leaf Ink's tints over paper are a Separation shading.
|
||||
let d = spot_gradient_doc(vec![
|
||||
spot_stop(0.0, "Leaf Ink", LEAF, 1.0),
|
||||
spot_stop(0.8, "Leaf Ink", LEAF, 0.5),
|
||||
spot_stop(0.2, "Gold Ink", GOLD, 1.0),
|
||||
paper(0.4),
|
||||
paper(1.0),
|
||||
]);
|
||||
let ps = text(&export(&d, &EpsOptions { level, ..opts(&d) }, None, None).unwrap());
|
||||
assert_eq!(dsc(&ps, "DocumentCustomColors").as_deref(), Some("(Leaf Ink)"), "{level:?}: {ps}");
|
||||
assert!(ps.contains("/VCsp0 [/Separation (Leaf Ink) cvn /DeviceCMYK") && !ps.contains("(Gold Ink) cvn"), "{level:?}: {ps}");
|
||||
checked_page(&ps);
|
||||
// An RGB stop out of order colors nothing either, so it keeps no ink out.
|
||||
let d = spot_gradient_doc(vec![
|
||||
spot_stop(0.0, "Gold Ink", GOLD, 1.0),
|
||||
spot_stop(0.8, "Gold Ink", GOLD, 0.5),
|
||||
GradientStop::new(0.2, Color::rgb(0.0, 1.0, 0.0)),
|
||||
paper(0.4),
|
||||
paper(1.0),
|
||||
]);
|
||||
let out = export(&d, &EpsOptions { level, ..opts(&d) }, None, None).unwrap();
|
||||
let ps = text(&out);
|
||||
assert!(ps.contains("/VCsp0 [/Separation (Gold Ink) cvn /DeviceCMYK"), "{level:?}: {ps}");
|
||||
assert!(!out.warnings.iter().any(|w| w.contains("spot")), "{level:?}: {:?}", out.warnings);
|
||||
// Gold Ink at a tint of 0 prints no ink.
|
||||
let d = spot_gradient_doc(vec![spot_stop(0.0, "Gold Ink", GOLD, 0.0), black(1.0)]);
|
||||
let ps = text(&export(&d, &EpsOptions { level, ..opts(&d) }, None, None).unwrap());
|
||||
assert_eq!(dsc(&ps, "DocumentCustomColors"), None, "{level:?}: {ps}");
|
||||
assert!(no_spot_space(&ps), "{level:?}: {ps}");
|
||||
}
|
||||
// Gold Ink at a tint of 0 over a 30% cyan plate (a DeviceN gradient read from a PDF) keeps
|
||||
// its process plate in the same space.
|
||||
let plated = GradientStop { plates: Some([0.3, 0.0, 0.0, 0.0]), ..spot_stop(1.0, "Gold Ink", GOLD, 0.0) };
|
||||
let d = spot_gradient_doc(vec![GradientStop::new(0.0, Color::cmyk(0.3, 0.0, 0.0, 0.0)), plated]);
|
||||
let ps = text(&export(&d, &opts(&d), None, None).unwrap());
|
||||
assert!(ps.contains("/VCdn0 [/DeviceN [/Cyan (Gold Ink) cvn ] /DeviceCMYK") && checked_page(&ps).contains("/C0 [0.3 0] /C1 [0.3 0]"), "{ps}");
|
||||
// EPS leaves stop opacity out, so a clear Gold Ink stop between clear stops is written opaque
|
||||
// and keeps its plate.
|
||||
let clear = |s: GradientStop| GradientStop { opacity: 0.0, ..s };
|
||||
let d = spot_gradient_doc(vec![black(0.0), clear(black(0.2)), clear(spot_stop(0.4, "Gold Ink", GOLD, 1.0)), clear(black(0.6)), black(1.0)]);
|
||||
let ps = text(&export(&d, &opts(&d), None, None).unwrap());
|
||||
assert_eq!(dsc(&ps, "DocumentCustomColors").as_deref(), Some("(Gold Ink)"), "{ps}");
|
||||
assert!(ps.contains("/VCdn0 [/DeviceN [/Black (Gold Ink) cvn ] /DeviceCMYK"), "{ps}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_inks_named_all_or_none_are_written_under_names_of_their_own() {
|
||||
let rect = |d: &mut Document, fill: &str, stroke: &str| {
|
||||
|
||||
@@ -90,16 +90,20 @@ impl Exporter<'_> {
|
||||
/// The stops of a gradient whose every stop is a tint of one spot ink (or paper white, 0% of
|
||||
/// it) as tints of that ink — a Separation shading — with midpoints as explicit stops (the
|
||||
/// tint and opacity there are halfway). A gradient that mixes a spot ink with other colors
|
||||
/// gets [`Self::device_n_stops`]. `None` for gradients without a spot ink.
|
||||
/// gets [`Self::device_n_stops`]. `None` for gradients without a spot ink that prints
|
||||
/// ([`Self::printed_spot`]).
|
||||
fn spot_stops(&mut self, g: &vectorcraft_color::Gradient) -> Option<Vec<(f32, krilla::color::Color, f32)>> {
|
||||
let ink = g.stops.iter().find_map(|s| s.swatch.as_deref().filter(|n| self.doc.swatch(n).is_some_and(|w| w.spot)))?;
|
||||
let drawn = g.drawn_stops();
|
||||
let ink = g.stops.iter().zip(&drawn).find_map(|(s, &d)| self.printed_spot(s, d))?;
|
||||
let tints: Option<Vec<f32>> = g
|
||||
.stops
|
||||
.iter()
|
||||
.map(|s| match s.swatch.as_deref() {
|
||||
.zip(&drawn)
|
||||
.map(|(s, &d)| match s.swatch.as_deref() {
|
||||
_ if !d => Some(0.0),
|
||||
// A stop that also prints process plates needs a DeviceN space.
|
||||
Some(n) if n == ink && s.plates.is_none() => Some(s.tint),
|
||||
_ if self.paper_stop(s) => Some(0.0),
|
||||
_ if self.inkless_stop(s) => Some(0.0),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
@@ -126,19 +130,22 @@ impl Exporter<'_> {
|
||||
/// the order the stops name them), with midpoints as explicit stops (the tints and opacity
|
||||
/// there are halfway), or for a Perceptual gradient the stops on its curve
|
||||
/// ([`vectorcraft_color::Gradient::ink_mixes`]). Paper white adds no ink; a spot stop with
|
||||
/// process plates ([`vectorcraft_color::GradientStop::plates`]) prints them too. `None`, with a
|
||||
/// warning, when a stop is written in RGB or Lab or the spot inks can't share a DeviceN space;
|
||||
/// the gradient is then written in the colors of its stops.
|
||||
/// process plates ([`vectorcraft_color::GradientStop::plates`]) prints them too. Only the stops
|
||||
/// that color something that draws ([`vectorcraft_color::Gradient::drawn_stops`]) name inks:
|
||||
/// the others are written with no ink. `None`, with a warning, when a stop is written in RGB or
|
||||
/// Lab or the spot inks can't share a DeviceN space; the gradient is then written in the colors
|
||||
/// of its stops.
|
||||
fn device_n_stops(&mut self, g: &vectorcraft_color::Gradient) -> Option<Vec<(f32, krilla::color::Color, f32)>> {
|
||||
use krilla::color::device_n::{Color as DnColor, Colorant, DeviceNSpace, Process};
|
||||
let doc = self.doc;
|
||||
fn spot_of<'s>(doc: &vectorcraft_doc::Document, s: &'s vectorcraft_color::GradientStop) -> Option<&'s str> {
|
||||
s.swatch.as_deref().filter(|n| doc.swatch(n).is_some_and(|w| w.spot))
|
||||
}
|
||||
let drawn = g.drawn_stops();
|
||||
// Each spot ink: its swatch's name, then its colorant name and CMYK equivalent.
|
||||
let mut spots: Vec<(&str, (String, [f32; 4]))> = Vec::new();
|
||||
for s in &g.stops {
|
||||
if let Some(n) = spot_of(doc, s)
|
||||
for (s, &d) in g.stops.iter().zip(&drawn) {
|
||||
if let Some(n) = self.printed_spot(s, d)
|
||||
&& !spots.iter().any(|(m, _)| *m == n)
|
||||
{
|
||||
spots.push((n, self.spot_ink(n)?));
|
||||
@@ -146,12 +153,15 @@ impl Exporter<'_> {
|
||||
}
|
||||
// Each stop's ink coverage: the four process inks, then one tint per spot ink.
|
||||
let mut inks: Vec<Vec<f32>> = Vec::with_capacity(g.stops.len());
|
||||
for s in &g.stops {
|
||||
for (s, &d) in g.stops.iter().zip(&drawn) {
|
||||
let mut v = vec![0.0; 4 + spots.len()];
|
||||
match spot_of(doc, s) {
|
||||
_ if !d => {}
|
||||
Some(n) => {
|
||||
let i = spots.iter().position(|(m, _)| *m == n)?;
|
||||
v[4 + i] = s.tint;
|
||||
// A spot ink that no drawn stop prints ([`Self::printed_spot`]) has no colorant.
|
||||
if let Some(i) = spots.iter().position(|(m, _)| *m == n) {
|
||||
v[4 + i] = s.tint;
|
||||
}
|
||||
if let Some(p) = s.plates {
|
||||
v[..4].copy_from_slice(&p.map(|t| t.clamp(0.0, 1.0)));
|
||||
}
|
||||
@@ -434,18 +444,38 @@ impl Exporter<'_> {
|
||||
!s.swatch.as_deref().is_some_and(|n| self.doc.swatch(n).is_some_and(|w| w.spot)) && paper(&s.color)
|
||||
}
|
||||
|
||||
/// The spot swatch whose tints are every stop of `g` that isn't paper white
|
||||
/// ([`Self::paper_stop`]); `None` when no stop is linked to a spot swatch or a stop has
|
||||
/// another color.
|
||||
fn one_spot_ink<'g>(&self, g: &'g vectorcraft_color::Gradient) -> Option<&'g str> {
|
||||
let spot = |s: &'g vectorcraft_color::GradientStop| s.swatch.as_deref().filter(|n| self.doc.swatch(n).is_some_and(|w| w.spot));
|
||||
let ink = g.stops.iter().find_map(spot)?;
|
||||
g.stops.iter().all(|s| (spot(s) == Some(ink) && s.plates.is_none()) || self.paper_stop(s)).then_some(ink)
|
||||
/// The spot swatch stop `s` prints: the one it is linked to, when the stop colors something
|
||||
/// that draws (`drawn`, [`vectorcraft_color::Gradient::drawn_stops`]) at a tint above 0 or
|
||||
/// with process plates ([`vectorcraft_color::GradientStop::plates`]), which print in the same
|
||||
/// DeviceN space.
|
||||
fn printed_spot<'s>(&self, s: &'s vectorcraft_color::GradientStop, drawn: bool) -> Option<&'s str> {
|
||||
s.swatch.as_deref().filter(|n| drawn && (s.tint > 0.0 || s.plates.is_some()) && self.doc.swatch(n).is_some_and(|w| w.spot))
|
||||
}
|
||||
|
||||
/// Can every stop of `g` be inks of a DeviceN space ([`Self::device_n_stops`]): is a stop
|
||||
/// linked to a spot swatch, and is every other stop paper white ([`Self::paper_stop`]) or
|
||||
/// written in CMYK or gray?
|
||||
/// Does stop `s` add no ink to a Separation or DeviceN shading: is it paper white
|
||||
/// ([`Self::paper_stop`]) or linked to a spot swatch at a tint of 0 without process plates?
|
||||
fn inkless_stop(&self, s: &vectorcraft_color::GradientStop) -> bool {
|
||||
let spot = s.swatch.as_deref().is_some_and(|n| self.doc.swatch(n).is_some_and(|w| w.spot));
|
||||
self.paper_stop(s) || (spot && self.printed_spot(s, true).is_none())
|
||||
}
|
||||
|
||||
/// The spot swatch whose tints are every stop of `g` that colors something that draws
|
||||
/// ([`vectorcraft_color::Gradient::drawn_stops`]) and adds ink ([`Self::inkless_stop`]); `None`
|
||||
/// when no such stop prints a spot swatch ([`Self::printed_spot`]) or one has another color.
|
||||
fn one_spot_ink<'g>(&self, g: &'g vectorcraft_color::Gradient) -> Option<&'g str> {
|
||||
let drawn = g.drawn_stops();
|
||||
let ink = g.stops.iter().zip(&drawn).find_map(|(s, &d)| self.printed_spot(s, d))?;
|
||||
g.stops
|
||||
.iter()
|
||||
.zip(&drawn)
|
||||
.all(|(s, &d)| !d || (s.swatch.as_deref() == Some(ink) && s.plates.is_none()) || self.inkless_stop(s))
|
||||
.then_some(ink)
|
||||
}
|
||||
|
||||
/// Can every stop of `g` be inks of a DeviceN space ([`Self::device_n_stops`]): does a stop
|
||||
/// print a spot swatch ([`Self::printed_spot`]), and is every other stop that colors something
|
||||
/// that draws linked to a spot swatch, paper white ([`Self::paper_stop`]) or written in CMYK or
|
||||
/// gray?
|
||||
fn device_n_inks_only(&self, g: &vectorcraft_color::Gradient) -> bool {
|
||||
let doc = self.doc;
|
||||
let cmyk_doc = doc.color_mode == vectorcraft_doc::ColorMode::Cmyk;
|
||||
@@ -453,7 +483,9 @@ impl Exporter<'_> {
|
||||
let inks = |s: &vectorcraft_color::GradientStop| {
|
||||
self.paper_stop(s) || matches!(self.out.color(&s.color, self.intent, cmyk_doc, self.convert), Color::Cmyk { .. } | Color::Gray { .. })
|
||||
};
|
||||
g.stops.iter().any(spot) && g.stops.iter().all(|s| spot(s) || inks(s))
|
||||
let drawn = g.drawn_stops();
|
||||
let mut stops = g.stops.iter().zip(&drawn);
|
||||
stops.clone().any(|(s, &d)| self.printed_spot(s, d).is_some()) && stops.all(|(s, &d)| !d || spot(s) || inks(s))
|
||||
}
|
||||
|
||||
/// Is `g` a Perceptual linear or radial gradient of spot inks, alone or mixed with process
|
||||
@@ -485,16 +517,20 @@ impl Exporter<'_> {
|
||||
let doc = self.doc;
|
||||
let full = doc.linked_color(doc.swatch(ink)?.paint.color()?, true);
|
||||
let space = self.separation(ink)?;
|
||||
// The stops keep their own tints; paper white is 0% of the ink.
|
||||
let own: Vec<f32> = g.stops.iter().map(|s| if s.swatch.as_deref() == Some(ink) { s.tint } else { 0.0 }).collect();
|
||||
// The stops keep their own tints; paper white, and a stop that colors nothing that draws,
|
||||
// is 0% of the ink.
|
||||
let own: Vec<f32> =
|
||||
g.stops.iter().zip(g.drawn_stops()).map(|(s, d)| if d && s.swatch.as_deref() == Some(ink) { s.tint } else { 0.0 }).collect();
|
||||
Some(g.ink_tints(&full, &own)?.into_iter().map(|(o, t, a)| (o, SepColor::new_f32(t, space.clone()).into(), a)).collect())
|
||||
}
|
||||
|
||||
/// Warn when gradient `g`, drawn as an image, has stops linked to a spot swatch: the image is
|
||||
/// in process colors, so the spot inks have no plate of their own.
|
||||
/// Warn when gradient `g`, drawn as an image, has stops that print a spot swatch
|
||||
/// ([`Self::printed_spot`]): the image is in process colors, so the spot inks have no plate of
|
||||
/// their own.
|
||||
fn warn_spot_image(&mut self, g: &vectorcraft_color::GradientPaint) {
|
||||
let from_stops = g.gradient.kind != GradientKind::Freeform || !g.has_freeform_points();
|
||||
if from_stops && self.has_spot_stop(&g.gradient) {
|
||||
let prints_spot = || g.gradient.stops.iter().zip(g.gradient.drawn_stops()).any(|(s, d)| self.printed_spot(s, d).is_some());
|
||||
if from_stops && prints_spot() {
|
||||
self.warn(
|
||||
"spot colors in gradients exported as images (freeform or dithered gradients, ones mixed around the hue wheel, and Perceptual ones with RGB or Lab stops or too many stops) are written in process colors",
|
||||
);
|
||||
|
||||
@@ -278,6 +278,60 @@ fn spot_gradients_keep_a_trace_of_process_ink_at_a_light_stop() {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`gold_gradient_pdf`] with spot swatch "Leaf" (C100 Y100) in the document too.
|
||||
fn gold_and_leaf_gradient_pdf(stops: Vec<GradientStop>, perceptual: bool) -> String {
|
||||
let mut d = doc(ColorMode::Cmyk, &[]);
|
||||
d.swatches.push(Swatch { name: "Gold".into(), paint: Paint::solid(Color::cmyk(0.0, 0.2, 0.8, 0.1)), global: true, spot: true });
|
||||
d.swatches.push(Swatch { name: "Leaf".into(), paint: Paint::solid(Color::cmyk(1.0, 0.0, 1.0, 0.0)), global: true, spot: true });
|
||||
let mut g = Gradient::new(GradientKind::Linear, stops);
|
||||
if perceptual {
|
||||
g.method = GradientMethod::Perceptual;
|
||||
}
|
||||
let paint = Paint::Gradient(Box::new(GradientPaint::new(g)));
|
||||
let l = d.layers[0].id;
|
||||
let n = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 110.0, 30.0)), Appearance::basic(paint, Paint::None, 0.0));
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
String::from_utf8_lossy(&export(&d, &PdfOptions::uncompressed()).unwrap()).into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradients_name_only_the_spot_inks_of_stops_that_color_what_draws() {
|
||||
let black = |offset: f32, opacity: f32| GradientStop { opacity, ..GradientStop::new(offset, Color::cmyk(0.0, 0.0, 0.0, 1.0)) };
|
||||
let paper = |offset: f32| GradientStop::new(offset, Color::cmyk(0.0, 0.0, 0.0, 0.0));
|
||||
let leaf = |offset: f32, tint: f32| {
|
||||
let mut s = GradientStop::new(offset, Color::cmyk(1.0, 0.0, 1.0, 0.0).tinted(tint));
|
||||
s.swatch = Some("Leaf".into());
|
||||
s.tint = tint;
|
||||
s
|
||||
};
|
||||
let clear_gold = GradientStop { opacity: 0.0, ..gold_stop(0.4, 1.0) };
|
||||
for perceptual in [false, true] {
|
||||
// Gold in a stretch where every stop is clear, and Gold out of order, where the stops at 0
|
||||
// and 0.8 cover the stretches next to it: Gold colors nothing, so no Gold plate.
|
||||
let clear = vec![black(0.0, 1.0), black(0.2, 0.0), clear_gold.clone(), black(0.6, 0.0), black(1.0, 1.0)];
|
||||
let out_of_order = vec![black(0.0, 1.0), black(0.8, 1.0), gold_stop(0.2, 1.0), black(0.4, 1.0), black(1.0, 1.0)];
|
||||
// Gold at a tint of 0 prints no Gold ink.
|
||||
let no_tint = vec![gold_stop(0.0, 0.0), black(1.0, 1.0)];
|
||||
for (what, stops) in [("clear", clear), ("out of order", out_of_order), ("tint 0", no_tint)] {
|
||||
let text = gold_gradient_pdf(stops, perceptual);
|
||||
assert!(!text.contains("/Gold"), "{what}, perceptual {perceptual}: {text}");
|
||||
assert!(!text.contains("/DeviceN") && !text.contains("/Separation"), "{what}, perceptual {perceptual}: {text}");
|
||||
}
|
||||
// Without the Gold stop out of order, Leaf's tints over paper are a Separation shading.
|
||||
let leaf_only = vec![leaf(0.0, 1.0), leaf(0.8, 0.5), gold_stop(0.2, 1.0), paper(0.4), paper(1.0)];
|
||||
let text = gold_and_leaf_gradient_pdf(leaf_only, perceptual);
|
||||
assert!(text.contains("[/Separation/Leaf/") && !text.contains("/Gold") && !text.contains("/DeviceN"), "perceptual {perceptual}: {text}");
|
||||
// A clear Gold stop next to an opaque one colors the stretch between them.
|
||||
let text = gold_gradient_pdf(vec![black(0.0, 1.0), clear_gold.clone(), black(1.0, 1.0)], perceptual);
|
||||
assert!(text.contains("[/DeviceN[/Black/Gold]"), "perceptual {perceptual}: {text}");
|
||||
}
|
||||
// Gold at a tint of 0 over a 30% cyan plate (a DeviceN gradient read from a PDF) keeps its
|
||||
// process plate in the same space.
|
||||
let plated = GradientStop { plates: Some([0.3, 0.0, 0.0, 0.0]), ..gold_stop(1.0, 0.0) };
|
||||
let text = gold_gradient_pdf(vec![GradientStop::new(0.0, Color::cmyk(0.3, 0.0, 0.0, 0.0)), plated], false);
|
||||
assert!(text.contains("[/DeviceN[/Cyan/Gold]"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_to_rgb_turns_cmyk_into_rg() {
|
||||
let ink = Color::cmyk(0.0, 1.0, 1.0, 0.0);
|
||||
|
||||
@@ -1644,6 +1644,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
A PDF, or a `.ai` read through its PDF part, whose spot inks have the same values but different names opens with a spot swatch for each ink. The solid fills and strokes of paths and type, and the stops of gradients on paths, link to the swatch of the ink they are painted in, the points of a gradient mesh in one of these inks keep the ink's color, and PDF export writes each of these inks as its own separation (`pdf/src/tests/import_color.rs`). Paint in these inks inside pattern cells, soft masks and type 3 glyphs, and gradients on type in these inks, still import as RGB. Type in one of these inks can also import as RGB when type in another Separation or DeviceN ink is shown with no path drawn between them. The stops of an axial or radial shading in a DeviceN space that mixes one spot ink with process plates (a spot ink over a constant cyan, for example) link to the ink's swatch at its tint and keep the process plates, and PDF and EPS (Level 3) export write those stops in a DeviceN space of the ink and those plates, so the spot plate is not lost (#3235, `import_color::a_device_n_gradient_of_a_spot_ink_over_a_process_plate_keeps_both_inks`). Solid fills that mix a spot ink with process plates, and stops that mix several spot inks, still import as RGB, with the spot inks kept as swatches.
|
||||
A `.ai` or EPS opened from its editing data keeps both swatches when a pattern's name, or a named color's, is also another swatch's: a pattern's swatch named as a default swatch, a named color or a palette color becomes "Name 2", and a named color named as a gradient or pattern swatch of the palette becomes "Name 2", with the fills, strokes and gradient stops linked to it following it (`ai::tests::a_pattern_swatch_named_as_another_swatch_is_numbered`, `ai::tests::a_color_named_as_a_palette_gradient_or_pattern_is_numbered`).
|
||||
PDF export writes a linear or radial gradient that mixes a spot color with process colors, or two spot colors, as a DeviceN shading of the spot inks and the process inks its stops use, so each spot ink prints on its own plate; an off-center midpoint is written as a stop with the inks halfway between its two stops. A gradient that mixes a spot color with RGB or Lab colors is still written without the spot ink, with a warning. A CMYK document, or an export that converts colors to a CMYK destination profile, writes RGB and Lab stops in CMYK first (unless the export converts colors to an RGB destination profile), so there such a gradient, Classic or Perceptual, is written as a DeviceN shading that keeps the spot ink. A Perceptual gradient of spot inks is written as a Separation or DeviceN shading of the stops on its curve; a dithered gradient fill, and a Perceptual gradient with RGB or Lab stops other than white or more than 4,096 stops on its curve, are written as images in process colors, with a warning. A Lab spot color in such a shading is named by the same Separation space, with the Lab alternate, as its flat fills (`engine/src/tests/linked_stops.rs`, `engine/src/tests/labspots.rs`).
|
||||
PDF and EPS export name a spot ink in a gradient's Separation or DeviceN space, and EPS in `%%DocumentCustomColors`, only when a stop linked to it at a tint above 0, or with process plates, colors part of the gradient (VectorCraft's own rule): a stop out of order whose neighboring stretches have no width, and in PDF a stop whose stretches are fully transparent, adds no ink and no empty plate. EPS writes every stop opaque, so there a transparent spot stop keeps its ink (`crates/pdf/src/tests/output.rs`, `crates/eps/src/tests.rs`).
|
||||
PDF export writes a spot color named All or None (in a Separation space, the names of every plate and of no plate: [ISO 32000-1:2008](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf), 8.6.6.4) as the ink All Spot or None Spot (All Spot 2 and on when a spot swatch already has that name) in its fills, strokes, gradients, colorized images and Lab alternates; Registration still prints on every plate (`pdf/src/tests/output.rs`).
|
||||
- `.vectorcraft` (lossless JSON, compressed, atomic saves, save down to v1), Save As with a format chooser, Save a Copy, Revert, templates, Data Recovery after a crash (browser tabs hold their copies with Web Locks too, so a background tab's copies are never taken for a crash's, and a tab repairs copies another removed when it resumes: #367), background save and export. The desktop app writes its warnings and errors (graphics adapter, a lost graphics device, panics the guard recovered from) to standard error and to `logs/vectorcraft.log` next to the preferences, the two previous runs kept and each file capped at 16 MiB, so a launch without a terminal still leaves something to attach to a bug report (#465). Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492). Saving to a network drive on macOS falls back to a plain fsync when the share refuses a full flush (#1625, part of #1620; not confirmed on a real share), and a save still succeeds when a folder refuses the temporary file, the rename over the document or copying permissions: the content is written elsewhere first and copied in, and a failed copy keeps the temporary file and names it (#1649, part of #1620). A document opened by a relative path finds its relatively linked images, and `vectorcraft-cli convert` lists what reading the input left out as `importWarnings` (#1606, fixes #1604).
|
||||
Preferences › File Handling › Data Recovery › Folder has a Choose… button on the desktop that picks the folder Data Recovery keeps its copies in ([Illustrator reference, Set Data Recovery preferences](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf)); a picked folder that Data Recovery cannot create its folders in or list is refused with a status message, and the field keeps its value. OK applies the same check to a folder typed into the field (VectorCraft's own rule): when it fails, the dialog stays open, the status bar says why and no preference changes; an empty field (the default folder) and the folder already saved are not checked. Preferences › Plug-ins & Scratch Disks is where additional plug-ins folders are set ([Illustrator preferences help](https://helpx.adobe.com/illustrator/desktop/get-started/preferences-and-settings/set-app-preferences.html)); its Additional Plug-ins Folder has a Choose… button on the desktop as well, VectorCraft's own, matching Data Recovery's, and VectorCraft's own rule refuses a picked or typed folder whose contents can't be listed. Additional Fonts Folder and Templates Folder stay typed only.
|
||||
|
||||
Reference in New Issue
Block a user