.ai import: spot colors in a kept brush's art become swatches

The reader kept a brush's art but dropped the colors it read from it, so
a spot color that only a Scatter or Art brush's art used made no swatch,
and the art's process colors did not count toward the document's color
mode.

The reader now keeps each brush art section's colors with its art. When
the document is finished, the colors of the art that a kept brush uses
are added after the rest of the file's, in the order the art was read:
its spot colors make swatches and its colors count toward the color
mode. Art that no kept brush uses (a pattern brush's tiles, a section
that a later one of the same name replaced, a section inside a symbol's
definition) brings none of its colors, and a spot color that the page's
art, a pattern or a symbol also defines keeps the value defined there.

Part of #637.
This commit is contained in:
dotCooCoo
2026-11-08 11:22:32 +07:00
committed by bflatastic
parent 77b72da70d
commit cbfca36c85
6 changed files with 95 additions and 6 deletions
+4 -1
View File
@@ -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 -3
View File
@@ -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,9 @@ 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`]).
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 +762,9 @@ 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 colors = BrushArtColors { spots: sub.spots, stop_spots: sub.stop_spots, cmyk: sub.cmyk_colors, rgb: sub.rgb_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
+20 -1
View File
@@ -946,6 +946,22 @@ 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.
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();
for (_, name) in names {
let Some(colors) = self.brush_art_colors.remove(&name) else { continue };
self.spots.extend(colors.spots);
self.stop_spots.extend(colors.stop_spots);
self.cmyk_colors += colors.cmyk;
self.rgb_colors += colors.rgb;
}
}
/// 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 +1072,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 +1096,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();
+13 -1
View File
@@ -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,18 @@ 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,
}
/// An image's operands, from `XN` and before `XI`.
#[derive(Debug, Default)]
pub(super) struct Raster {
+50
View File
@@ -7797,6 +7797,56 @@ 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());
}
/// 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