mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 20:01:37 +00:00
Merge pull request #6006 from dotCooCoo/fix/ai-brush-art-colors
.ai import: spot colors in a kept brush's art become swatches
This commit is contained in:
@@ -70,6 +70,9 @@ const TOOLS: [(&str, Tool); 5] = [
|
||||
pub(super) const MAX_BRUSHES: usize = 4096;
|
||||
/// Most brush art sections read from one file (a pattern brush has up to five).
|
||||
pub(super) const MAX_BRUSH_ART: usize = 5 * MAX_BRUSHES;
|
||||
/// Most named colors kept from one brush art section, and brought in from the art of one file's
|
||||
/// brushes, each name once per section: the rest are left out.
|
||||
pub(super) const MAX_BRUSH_ART_COLORS: usize = 20_000;
|
||||
/// Most [`art_weight`] copied into the brushes of one file, each brush counting its own copy.
|
||||
const MAX_BRUSH_ART_NODES: usize = 250_000;
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ pub use document::{
|
||||
};
|
||||
pub(crate) use document::{slot_frame, slot_of};
|
||||
pub use types::IMAGE_TOO_LARGE;
|
||||
use types::{BLEND_BUDGET, FLIP, Frame, GState, Graph, Kind, PatternHead, PendingEnvelope, Place, Raster, SymbolHead};
|
||||
use types::{BLEND_BUDGET, BrushArtColors, FLIP, Frame, GState, Graph, Kind, PatternHead, PendingEnvelope, Place, Raster, SymbolHead};
|
||||
#[cfg(test)]
|
||||
pub use types::{GRAPH_PLAIN, GRAPH_UNENDED, LINKED_PLACEHOLDERS, NON_NATIVE_ART, NON_NATIVE_ART_UNREAD, VERSIONED_UNENDED};
|
||||
pub(super) use types::{blend_mode, blend_name};
|
||||
@@ -587,6 +587,8 @@ struct Reader<'a> {
|
||||
brush_drafts: Vec<super::brush::BrushDraft>,
|
||||
/// The art of the brushes (`%AI8_BeginBrushPattern`), by name.
|
||||
brush_art: HashMap<String, Node>,
|
||||
/// The colors of the art in [`Reader::brush_art`], by name.
|
||||
brush_art_colors: HashMap<String, BrushArtColors>,
|
||||
/// The brush art sections read so far, repeated names included.
|
||||
brush_art_sections: usize,
|
||||
/// What the brushes and brush art read so far give a brushed stroke.
|
||||
@@ -737,6 +739,7 @@ impl<'a> Reader<'a> {
|
||||
brush_order: None,
|
||||
brush_drafts: Vec::new(),
|
||||
brush_art: HashMap::new(),
|
||||
brush_art_colors: HashMap::new(),
|
||||
brush_art_sections: 0,
|
||||
brushes_kept: BrushesKept::default(),
|
||||
brushes_unread: 0,
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::types::{
|
||||
FLIP, GState, Kind, MAX_DEPTH, MAX_HEX, MAX_LISTED, MAX_NODES, MAX_NOTE, MAX_PATTERNS, MAX_PLACED_FOLDER, MAX_STACK, MAX_SUBREADERS, MAX_SYMBOLS,
|
||||
PatternHead, Place, Raster, SKIPPED, SymbolHead, VERSIONED_UNENDED,
|
||||
};
|
||||
use super::{Arc, Global, Lexer, Node, NodeKind, PatternDef, Reader, Rect, Tok, V, marker, obj};
|
||||
use super::{Arc, BrushArtColors, Global, Lexer, Node, NodeKind, PatternDef, Reader, Rect, Tok, V, marker, obj};
|
||||
use vectorcraft_doc::{NodeId, Symbol, SymbolRegistration};
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
@@ -714,7 +714,10 @@ impl<'a> Reader<'a> {
|
||||
|
||||
/// A brush's art (`section`: what follows `%AI8_BeginBrushPattern`, its name as a string, then
|
||||
/// the art), read by a reader of its own as a pattern's is, in a space with y down. Art it can't
|
||||
/// read whole isn't kept: the brushes that use it aren't read.
|
||||
/// read whole isn't kept: the brushes that use it aren't read. Kept art's colors are kept with
|
||||
/// it, and count as the page's once a brush that is kept uses the art
|
||||
/// ([`Reader::count_brush_art_colors`]): each name's first definition in the art, at most
|
||||
/// [`brush::MAX_BRUSH_ART_COLORS`] names.
|
||||
fn define_brush_art(&mut self, section: &[u8]) {
|
||||
if self.brush_art_sections >= brush::MAX_BRUSH_ART || self.subreader_depth >= MAX_SUBREADERS {
|
||||
return;
|
||||
@@ -760,7 +763,10 @@ impl<'a> Reader<'a> {
|
||||
_ => Some(Node::group(self.doc.alloc_id(), art)),
|
||||
};
|
||||
let Some(node) = node else { return };
|
||||
self.brush_art.insert(name, node);
|
||||
self.brush_art.insert(name.clone(), node);
|
||||
let mut colors = BrushArtColors { spots: sub.spots, stop_spots: sub.stop_spots, cmyk: sub.cmyk_colors, rgb: sub.rgb_colors };
|
||||
colors.keep_first(brush::MAX_BRUSH_ART_COLORS);
|
||||
self.brush_art_colors.insert(name, colors);
|
||||
}
|
||||
|
||||
/// Lend `sub`, a reader of a definition, what this import may still spend on laying out blends
|
||||
|
||||
@@ -946,6 +946,41 @@ impl<'a> Reader<'a> {
|
||||
Some((lib, left_out + self.brushes_unread))
|
||||
}
|
||||
|
||||
/// Count the colors of the brush art that the brushes `lib` use after the colors read so far,
|
||||
/// in the order the art was read: its spot colors make swatches, and its colors count toward
|
||||
/// the color mode. Art that no brush of `lib` uses brings none of its colors. The art brings
|
||||
/// at most [`brush::MAX_BRUSH_ART_COLORS`] different color names in all, each name's color
|
||||
/// operators before its gradient stops, as [`Self::file_swatches`] takes them; its colors past
|
||||
/// them still count toward the color mode.
|
||||
fn count_brush_art_colors(&mut self, lib: &[vectorcraft_brush::Brush]) {
|
||||
let used: std::collections::HashSet<NodeId> = lib.iter().flat_map(|b| b.art()).map(|a| a.id).collect();
|
||||
let mut names: Vec<(NodeId, String)> = self.brush_art.iter().filter(|(_, a)| used.contains(&a.id)).map(|(n, a)| (a.id, n.clone())).collect();
|
||||
names.sort();
|
||||
// The names brought so far: every section's color operators', then every section's stops'.
|
||||
let mut brought = std::collections::HashSet::new();
|
||||
let mut stops = vec![];
|
||||
for (_, name) in names {
|
||||
let Some(colors) = self.brush_art_colors.remove(&name) else { continue };
|
||||
for n in colors.spots {
|
||||
if brought.contains(&n.name) || brought.len() >= brush::MAX_BRUSH_ART_COLORS {
|
||||
continue;
|
||||
}
|
||||
brought.insert(n.name.clone());
|
||||
self.spots.push(n);
|
||||
}
|
||||
stops.push(colors.stop_spots);
|
||||
self.cmyk_colors += colors.cmyk;
|
||||
self.rgb_colors += colors.rgb;
|
||||
}
|
||||
for n in stops.into_iter().flatten() {
|
||||
if brought.contains(&n.name) || brought.len() >= brush::MAX_BRUSH_ART_COLORS {
|
||||
continue;
|
||||
}
|
||||
brought.insert(n.name.clone());
|
||||
self.stop_spots.push(n);
|
||||
}
|
||||
}
|
||||
|
||||
/// The graphic styles of the style list, in its order: each listed name the art styles define with
|
||||
/// an appearance [`appearance_from_art_style`] reads, once (a name listed again, empty or not
|
||||
/// defined is left out), with ids from 1. Also the id of each kept name.
|
||||
@@ -1056,6 +1091,10 @@ impl<'a> Reader<'a> {
|
||||
};
|
||||
take_ruler_guides(&mut doc.layers, &pick, &mut doc.guides, 0);
|
||||
}
|
||||
let mut brushes = self.brush_library();
|
||||
if let Some((lib, _)) = &brushes {
|
||||
self.count_brush_art_colors(lib);
|
||||
}
|
||||
if self.color_model.unwrap_or(self.cmyk_colors > self.rgb_colors) {
|
||||
doc.color_mode = ColorMode::Cmyk;
|
||||
let (swatches, groups) = vectorcraft_color::default_swatches(ColorMode::Cmyk.model());
|
||||
@@ -1076,7 +1115,6 @@ impl<'a> Reader<'a> {
|
||||
if let Some(author) = self.author.take() {
|
||||
doc.metadata.author = author;
|
||||
}
|
||||
let mut brushes = self.brush_library();
|
||||
// The file's graphic styles in place of the defaults, and the objects linked to them.
|
||||
let (styles, ids) = self.graphic_styles();
|
||||
let styles_read = !styles.is_empty();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Affine, BlendMode, Dash, FillRule, HAIRLINE, Knockout, LayerColor, LineCap, LineJoin, Node, Obj, Paint, Rect, V};
|
||||
use super::{Affine, BlendMode, Dash, FillRule, HAIRLINE, Knockout, LayerColor, LineCap, LineJoin, Named, Node, Obj, Paint, Rect, V};
|
||||
|
||||
/// Deepest nesting of layers, groups and dictionaries.
|
||||
pub(super) const MAX_DEPTH: usize = 256;
|
||||
@@ -249,6 +249,29 @@ pub(super) struct SymbolHead {
|
||||
pub(super) missing_stroke: bool,
|
||||
}
|
||||
|
||||
/// The colors a brush's art uses, counted as the page's are once a brush that is kept uses the art.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct BrushArtColors {
|
||||
/// The named colors its color operators define.
|
||||
pub(super) spots: Vec<Named>,
|
||||
/// The named colors its gradient stops name.
|
||||
pub(super) stop_spots: Vec<Named>,
|
||||
/// Its colors set in each model.
|
||||
pub(super) cmyk: usize,
|
||||
pub(super) rgb: usize,
|
||||
}
|
||||
|
||||
impl BrushArtColors {
|
||||
/// Keep each name's first definition only, its color operators' before its gradient stops',
|
||||
/// and at most `limit` names in all. The process-color counts stay as they are.
|
||||
pub(super) fn keep_first(&mut self, limit: usize) {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut keep = |n: &Named| seen.len() < limit && seen.insert(n.name.clone());
|
||||
self.spots.retain(|n| keep(n));
|
||||
self.stop_spots.retain(|n| keep(n));
|
||||
}
|
||||
}
|
||||
|
||||
/// An image's operands, from `XN` and before `XI`.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct Raster {
|
||||
|
||||
@@ -7797,6 +7797,118 @@ fn a_brushs_art_takes_the_patterns_defined_before_it() {
|
||||
assert!(matches!(fill(&s.art), Paint::Pattern { ref pattern, .. } if pattern == "Dots"), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A spot color that only a brush's art uses comes in as a swatch, and the art's process colors
|
||||
/// count toward the document's color mode, as a pattern's do.
|
||||
#[test]
|
||||
fn a_brushs_art_brings_its_spot_colors_and_color_mode() {
|
||||
let section = format!("%AI8_BeginBrushPattern\n(Dot art)\n0 A\n0 1 1 0 (Ink) 0 x\n{}f\n%AI8_EndBrushPattern\n", rect_path(0.0, 0.0, 4.0, 4.0));
|
||||
let r = read_ok(&format!("{section}{}{}", plugin_object(SCATTER, "Spray", SPRAY), layer("")));
|
||||
let lib = vectorcraft_brush::library(&r.doc);
|
||||
assert!(matches!(brush_named(&lib, "Spray"), vectorcraft_brush::BrushKind::Scatter(_)), "{:?}", r.warnings);
|
||||
let ink = r.doc.swatch("Ink").map(|s| (s.paint.color(), s.global, s.spot));
|
||||
assert_eq!(ink, Some((Some(Color::cmyk(0.0, 1.0, 1.0, 0.0)), true, true)), "{:?}", r.warnings);
|
||||
assert_eq!(r.doc.color_mode, ColorMode::Cmyk);
|
||||
// Art that isn't kept brings nothing.
|
||||
let odd = "%AI8_BeginBrushPattern\n(Odd art)\n0 1 1 0 (Lost ink) 0 x\n0 0 m\n1 1 L\nnotanoperator\nf\n%AI8_EndBrushPattern\n";
|
||||
let r = read_ok(&format!("{odd}{}{}", plugin_object(SCATTER, "Odd", &SPRAY.replace("Dot art", "Odd art")), layer("")));
|
||||
assert!(r.doc.swatch("Lost ink").is_none());
|
||||
assert_eq!(r.doc.color_mode, ColorMode::Rgb);
|
||||
// Nor does art that only a brush left out uses (a pattern brush).
|
||||
let tile = "%AI8_BeginBrushPattern\n(Tile one)\n0 A\n0 1 1 0 (Fence ink) 0 x\n0 0 m\n4 0 L\n4 4 L\nf\n%AI8_EndBrushPattern\n";
|
||||
let r = read_ok(&format!("{tile}{}{}", plugin_object(PATTERN_BRUSH, "Fence", FENCE), layer("")));
|
||||
assert!(vectorcraft_brush::library(&r.doc).is_empty(), "{:?}", r.warnings);
|
||||
assert!(r.doc.swatch("Fence ink").is_none());
|
||||
assert_eq!(r.doc.color_mode, ColorMode::Rgb);
|
||||
}
|
||||
|
||||
/// A spot color that the page's art and a brush's art both define keeps the page's value, and a
|
||||
/// brush art section that a later one of the same name replaces brings none of its colors.
|
||||
#[test]
|
||||
fn a_brushs_art_colors_come_after_the_pages_and_a_replaced_sections_bring_nothing() {
|
||||
let section = |ink: &str, color: &str| {
|
||||
format!("%AI8_BeginBrushPattern\n(Dot art)\n0 A\n{color} ({ink}) 0 x\n{}f\n%AI8_EndBrushPattern\n", rect_path(0.0, 0.0, 4.0, 4.0))
|
||||
};
|
||||
let page = layer(&format!("0 0 1 0 (Ink) 0 x\n{}f", rect_path(0.0, 0.0, 10.0, 10.0)));
|
||||
let body = format!("{}{}{}{page}", section("Gone ink", "1 0 0 0"), section("Ink", "0 1 1 0"), plugin_object(SCATTER, "Spray", SPRAY));
|
||||
let r = read_ok(&body);
|
||||
assert!(matches!(brush_named(&vectorcraft_brush::library(&r.doc), "Spray"), vectorcraft_brush::BrushKind::Scatter(_)));
|
||||
assert_eq!(r.doc.swatch("Ink").and_then(|s| s.paint.color()), Some(Color::cmyk(0.0, 0.0, 1.0, 0.0)), "{:?}", r.warnings);
|
||||
assert!(r.doc.swatch("Gone ink").is_none());
|
||||
}
|
||||
|
||||
/// The art of a file's brushes brings each color name's first definition once, and at most
|
||||
/// [`super::brush::MAX_BRUSH_ART_COLORS`] names; the colors it leaves out still count toward the
|
||||
/// color mode, and art that no brush uses, or that a later section replaces, takes no places.
|
||||
#[test]
|
||||
fn brush_art_keeps_each_color_name_once_and_a_bounded_number_of_names() {
|
||||
let open = |sections: &[(&str, &str)]| {
|
||||
let defs: String = sections
|
||||
.iter()
|
||||
.map(|(name, colors)| {
|
||||
format!("%AI8_BeginBrushPattern\n({name})\n0 A\n{colors}{}f\n%AI8_EndBrushPattern\n", rect_path(0.0, 0.0, 4.0, 4.0))
|
||||
})
|
||||
.collect();
|
||||
let mist = plugin_object(SCATTER, "Mist", &SPRAY.replace("Dot art", "Dash art"));
|
||||
read_ok(&format!("{defs}{}{mist}{}", plugin_object(SCATTER, "Spray", SPRAY), layer(""))).doc
|
||||
};
|
||||
let max = super::brush::MAX_BRUSH_ART_COLORS;
|
||||
let ink = "0 1 1 0 (Ink) 0 x\n";
|
||||
let many: String = (0..max).map(|i| format!("1 0 0 0 (Name {i}) 0 x\n")).collect();
|
||||
// One name repeated takes one place.
|
||||
let doc = open(&[("Dot art", &format!("{}{ink}", "1 0 0 0 (Same) 0 x\n".repeat(max)))]);
|
||||
assert!(doc.swatch("Same").is_some() && doc.swatch("Ink").is_some());
|
||||
// Different names fill the places: the name past them is left out, and its color still counts.
|
||||
let doc = open(&[("Dot art", &format!("{many}{ink}"))]);
|
||||
assert!(doc.swatch("Name 0").is_some() && doc.swatch("Ink").is_none());
|
||||
assert_eq!(doc.color_mode, ColorMode::Cmyk);
|
||||
// The places are shared by the art of every brush, and a name in the art of two takes one.
|
||||
let doc = open(&[("Dot art", &many), ("Dash art", ink)]);
|
||||
assert_eq!(vectorcraft_brush::library(&doc).len(), 2);
|
||||
assert!(doc.swatch("Name 0").is_some() && doc.swatch("Ink").is_none());
|
||||
let half: String = many.lines().take(max / 2).map(|l| format!("{l}\n")).collect();
|
||||
let doc = open(&[("Dot art", &half), ("Dash art", &format!("{half}{ink}"))]);
|
||||
assert!(doc.swatch("Name 0").is_some() && doc.swatch("Ink").is_some());
|
||||
// The color operators of every brush's art take places before the gradient stops of any.
|
||||
let stops: String = (0..max / 250)
|
||||
.map(|g| {
|
||||
let named: String = (0..250).map(|s| format!("%_0 0 1 0 (Stop {g} {s}) 0 0 5 1 6 50 {} Bs\n", s * 100 / 250)).collect();
|
||||
format!("%AI5_BeginGradient: (G{g})\n(G{g}) 0 250 Bd\n[\n<00FF>\n0\n{named}BD\n%AI5_EndGradient\n")
|
||||
})
|
||||
.collect();
|
||||
let doc = open(&[("Dot art", &stops), ("Dash art", ink)]);
|
||||
assert!(doc.swatch("Stop 0 0").is_some() && doc.swatch("Ink").is_some(), "{:?}", doc.swatch("Stop 0 0"));
|
||||
// Art that no brush uses, and art that a later section of its name replaces, take no places.
|
||||
let doc = open(&[("Other art", &many), ("Dot art", &many), ("Dot art", ink)]);
|
||||
assert!(doc.swatch("Ink").is_some() && doc.swatch("Name 0").is_none());
|
||||
}
|
||||
|
||||
/// A global process color that only a brush's art defines with its color operator stays a process
|
||||
/// color when a gradient of the file names it as a stop, as the page's own definition does (#1771).
|
||||
#[test]
|
||||
fn a_brushs_art_defines_a_process_color_that_a_gradient_stop_names() {
|
||||
let defs = "%AI5_BeginGradient: (Brand)\n(Brand) 0 2 Bd\n[\n<00FF>\n0\n\
|
||||
%_0.93 0.1 0 0 (Test Cyan) 0 0 5 1 6 50 0 Bs\n%_0 0 0 0 1 0 0 2 1 6 50 100 Bs\nBD\n%AI5_EndGradient\n";
|
||||
let section = format!(
|
||||
"%AI8_BeginBrushPattern\n(Dot art)\n0 A\n0.93 0.1 0 0 (Test Cyan) 0 0 Xk\n{}f\n%AI8_EndBrushPattern\n",
|
||||
rect_path(0.0, 0.0, 4.0, 4.0)
|
||||
);
|
||||
let doc = read_ok(&format!("{defs}{section}{}{}", plugin_object(SCATTER, "Spray", SPRAY), layer(""))).doc;
|
||||
assert!(matches!(brush_named(&vectorcraft_brush::library(&doc), "Spray"), vectorcraft_brush::BrushKind::Scatter(_)));
|
||||
let sw = doc.swatch("Test Cyan").unwrap();
|
||||
assert!(sw.global && !sw.spot, "a global process color, not an ink: {sw:?}");
|
||||
}
|
||||
|
||||
/// A brush art section inside a symbol's definition isn't kept, so its spot colors make no swatch.
|
||||
#[test]
|
||||
fn brush_art_inside_a_symbol_brings_no_colors() {
|
||||
let section =
|
||||
format!("%AI8_BeginBrushPattern\n(Dot art)\n0 A\n0 1 1 0 (Hidden ink) 0 x\n{}f\n%AI8_EndBrushPattern\n", rect_path(0.0, 0.0, 4.0, 4.0));
|
||||
let body = format!("{}{}{}", symbol("S", &format!("{section}{}", dot()), 5), plugin_object(SCATTER, "Spray", SPRAY), layer(""));
|
||||
let r = read_ok(&body);
|
||||
assert!(r.doc.symbols.iter().any(|s| s.name == "S"), "{:?}", r.warnings);
|
||||
assert!(r.doc.swatch("Hidden ink").is_none());
|
||||
}
|
||||
|
||||
/// A brush's art filled with a pattern defined in the version 7 form places the tiles with the
|
||||
/// fill's offset, scale, rotation, reflection and shear operands, and a brush whose art has such a
|
||||
/// fill without all nine of them isn't read.
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user