diff --git a/crates/engine/src/cmd/docinfo.rs b/crates/engine/src/cmd/docinfo.rs index cb1bb86fbd..7499e14943 100644 --- a/crates/engine/src/cmd/docinfo.rs +++ b/crates/engine/src/cmd/docinfo.rs @@ -18,7 +18,7 @@ pub fn specs() -> Vec { "Document Info", ["Window", "Document Info"], None, - "{selectionOnly?, category?: document|objects|graphicStyles|brushes|spotColors|patterns|gradients|symbols|fonts|fontDetails|linkedImages|embeddedImages|repeats (only that one in sections and the text), format?: \"text\" (→ {text}: the report Document Info › Save… and Package write)} → {document, objects: {paths, compoundPaths, groups, …}, pathLength: pt (the paths' total length, compound paths' included, guides left out), fonts, images, swatches, graphicStyleNames (with selectionOnly: the styles the selected objects are linked to), brushNames (the Brushes panel's brushes; with selectionOnly: the brushes the selected objects' strokes use), repeatObjects: [{id, name, type: radial|grid|mirror, instances, rows?, cols?}] (the Repeat objects, those in groups and other repeats included; with selectionOnly: those in the selection), patternObjects: [{id, name, patterns}] (the objects filled or stroked with a pattern, each once, those in groups and type whose drawn characters use one included, hidden fills and strokes, strokes with no width, guides, compound path members' own fills and strokes, images' fills and strokes without a shape and characters that draw nothing left out, with the patterns they use, which objects.patterns counts; with selectionOnly: those in the selection), patternNames (the patterns those objects use), …, sections: [{id, title, rows: [[label, value]]}] (the panel's categories; fontDetails: each font's file and whether its license lets it be embedded)}", + "{selectionOnly?, category?: document|objects|graphicStyles|brushes|spotColors|patterns|gradients|symbols|fonts|fontDetails|linkedImages|embeddedImages|repeats (only that one in sections and the text), format?: \"text\" (→ {text}: the report Document Info › Save… and Package write)} → {document, objects: {paths, compoundPaths, groups, …}, pathLength: pt (the paths' total length, compound paths' included, guides left out), fonts, images, swatches, graphicStyleNames (with selectionOnly: the styles the selected objects are linked to), brushNames (the Brushes panel's brushes; with selectionOnly: the brushes the selected objects' strokes use), repeatObjects: [{id, name, type: radial|grid|mirror, instances, rows?, cols?}] (the Repeat objects, those in groups and other repeats included; with selectionOnly: those in the selection), patternObjects: [{id, name, patterns}] (the objects filled or stroked with a pattern, each once, those in groups and type whose drawn characters use one included, hidden fills and strokes, strokes with no width, guides, compound path members' own fills and strokes, images' fills and strokes without a shape and characters that draw nothing left out, with the patterns they use, which objects.patterns counts; with selectionOnly: those in the selection), patternNames (the patterns those objects use), gradientObjects: [{id, name, gradients}] (the objects filled or stroked with a gradient, by the same rules, with the gradients they use: a linked gradient swatch's name, else Linear gradient, Radial gradient or Freeform gradient; objects.gradients counts them), gradientNames (the gradients those objects use), …, sections: [{id, title, rows: [[label, value]]}] (the panel's categories; fontDetails: each font's file and whether its license lets it be embedded)}", has_doc, info ), @@ -42,7 +42,7 @@ pub const CATEGORIES: [(&str, &str); 13] = [ ("brushes", "Brushes"), ("spotColors", "Spot Colors"), ("patterns", "Pattern Objects"), - ("gradients", "Gradient Swatches"), + ("gradients", "Gradient Objects"), ("symbols", "Symbols"), ("fonts", "Fonts"), ("fontDetails", "Font Details"), @@ -131,7 +131,8 @@ fn repeat_entry(d: &Document, n: &Node, spec: &RepeatSpec) -> (Value, (String, S /// The Document Info categories of `d` (only `category` when given) from `info` (what /// `document.info` reports), its fonts, its image objects, the rows of its Repeat objects and the -/// rows of its objects filled or stroked with a pattern. +/// rows of its objects filled or stroked with a pattern and with a gradient. +#[allow(clippy::too_many_arguments)] fn sections( d: &Document, info: &Value, @@ -139,6 +140,7 @@ fn sections( images: &[ImageRow], repeats: &[(String, String)], pattern_objects: &[(String, String)], + gradient_objects: &[(String, String)], category: Option<&str>, ) -> Vec
{ let names = |k: &str| -> Vec<(String, String)> { @@ -183,7 +185,7 @@ fn sections( "brushes" => names("brushNames"), "spotColors" => names("spotColors"), "patterns" => pattern_objects.to_vec(), - "gradients" => d.swatches_iter().filter(|s| matches!(s.paint, Paint::Gradient(_))).map(|s| (s.name.clone(), String::new())).collect(), + "gradients" => gradient_objects.to_vec(), "symbols" => names("symbols"), "fonts" => fonts.iter().map(|f| (super::fonts::font_label(f), String::new())).collect(), "fontDetails" => { @@ -254,19 +256,37 @@ pub fn report(s: &mut Session, selection_only: bool) -> Result { Ok(v["text"].as_str().unwrap_or_default().to_string()) } -/// The type object `n` holds and the runs its layout draws, when it uses a pattern: the runs with a -/// glyph outline (list markers and ruby included) or an underline or strikethrough bar. Spaces and -/// line breaks draw no outline. A type object's characters carry their own fill and stroke, and the -/// type object's own fills and strokes paint on its characters, so only those runs paint. The type -/// is laid out with its inline graphics' bounds resolved from `d`'s symbols, as the renderer lays it out. +/// The pattern paint `p` uses: the pattern's name. +fn pattern_label(p: &Paint) -> Option { + match p { + Paint::Pattern { pattern, .. } => Some(pattern.clone()), + _ => None, + } +} + +/// The gradient paint `p` uses: the name of the gradient swatch it is linked to, else its kind +/// ("Linear gradient", "Radial gradient" or "Freeform gradient"). +fn gradient_label(p: &Paint) -> Option { + match p { + Paint::Gradient(g) => Some(g.swatch.clone().filter(|s| !s.is_empty()).unwrap_or_else(|| format!("{} gradient", g.gradient.kind.label()))), + _ => None, + } +} + +/// The type object `n` holds and the runs its layout draws, when it uses a pattern or a gradient: +/// the runs with a glyph outline (list markers and ruby included) or an underline or strikethrough +/// bar. Spaces and line breaks draw no outline. A type object's characters carry their own fill and +/// stroke, and the type object's own fills and strokes paint on its characters, so only those runs +/// paint. The type is laid out with its inline graphics' bounds resolved from `d`'s symbols, as the +/// renderer lays it out. fn painted_runs<'a>(d: &Document, n: &'a Node) -> Option<(&'a vectorcraft_doc::TextObject, BTreeSet)> { let NodeKind::Text(t) = &n.kind else { return None }; - let is_pattern = |p: &Paint| matches!(p, Paint::Pattern { .. }); + let is_listed = |p: &Paint| matches!(p, Paint::Pattern { .. } | Paint::Gradient(_)); let own = n.appearance.items.iter().any(|i| match i { - vectorcraft_doc::AppearanceItem::Fill(f) => is_pattern(&f.paint), - vectorcraft_doc::AppearanceItem::Stroke(s) => is_pattern(&s.paint), + vectorcraft_doc::AppearanceItem::Fill(f) => is_listed(&f.paint), + vectorcraft_doc::AppearanceItem::Stroke(s) => is_listed(&s.paint), }); - if !own && !t.runs.iter().any(|r| is_pattern(&r.style.fill) || is_pattern(&r.style.stroke)) { + if !own && !t.runs.iter().any(|r| is_listed(&r.style.fill) || is_listed(&r.style.stroke)) { return None; } let db = vectorcraft_text::FontDb::global(); @@ -277,42 +297,38 @@ fn painted_runs<'a>(d: &Document, n: &'a Node) -> Option<(&'a vectorcraft_doc::T Some((t, painted)) } -/// The patterns `n`'s own fills and strokes paint, each once. A hidden fill or stroke and a stroke -/// with no width paint nothing, and neither does any fill or stroke of a guide (drawn in the guide -/// color), of a compound path's member (`members`: the compound path's own fills and strokes paint -/// it) or of type whose layout draws nothing (`painted_runs`). An image's fills and strokes paint -/// only where an effect gives them a shape. -fn own_patterns<'a>( +/// The patterns or gradients (`label`: [`pattern_label`] or [`gradient_label`]) that `n`'s own +/// fills and strokes paint, each once. A hidden fill or stroke and a stroke with no width paint +/// nothing, and neither does any fill or stroke of a guide (drawn in the guide color), of a compound +/// path's member (`members`: the compound path's own fills and strokes paint it) or of type whose +/// layout draws nothing (`painted_runs`). An image's fills and strokes paint only where an effect +/// gives them a shape. +fn own_paints( d: &Document, - n: &'a Node, + n: &Node, members: &HashSet<*const Node>, painted_runs: Option<&(&vectorcraft_doc::TextObject, BTreeSet)>, -) -> Vec<&'a str> { + label: fn(&Paint) -> Option, +) -> Vec { if matches!(n.kind, NodeKind::Path { guide: true, .. }) || members.contains(&std::ptr::from_ref(n)) || painted_runs.is_some_and(|(_, r)| r.is_empty()) + || !n.appearance.items.iter().any(|i| label(i.paint()).is_some()) { return vec![]; } - // The patterns an image's shaped fills and strokes paint (their effects can transform the - // pattern, so they are matched by name). - let shaped: Option> = matches!(n.kind, NodeKind::Image(_)).then(|| { - vectorcraft_render::effects::image_paint(Some(d), n) - .into_iter() - .filter_map(|(_, _, item)| match item.paint() { - Paint::Pattern { pattern, .. } => Some(pattern.clone()), - _ => None, - }) - .collect() - }); - let mut out: Vec<&str> = vec![]; + // The patterns or gradients an image's shaped fills and strokes paint (their effects can + // transform the paint, so they are matched by label). + let shaped: Option> = matches!(n.kind, NodeKind::Image(_)) + .then(|| vectorcraft_render::effects::image_paint(Some(d), n).into_iter().filter_map(|(_, _, item)| label(item.paint())).collect()); + let mut out: Vec = vec![]; for item in &n.appearance.items { - let Paint::Pattern { pattern, .. } = item.paint() else { continue }; + let Some(name) = label(item.paint()) else { continue }; let paints = item.visible() && !matches!(item, vectorcraft_doc::AppearanceItem::Stroke(s) if s.width <= 0.0) - && shaped.as_ref().is_none_or(|s| s.contains(pattern)); - if paints && !out.contains(&pattern.as_str()) { - out.push(pattern); + && shaped.as_ref().is_none_or(|s| s.contains(&name)); + if paints && !out.contains(&name) { + out.push(name); } } out @@ -354,6 +370,11 @@ fn info(s: &mut Session, p: &Value) -> Result { // `pattern_objects` and `pattern_rows` hold the `patternObjects` entries and the rows of the objects filled or stroked with a pattern, in walk order. let mut pattern_objects: Vec = vec![]; let mut pattern_rows: Vec<(String, String)> = vec![]; + // `gradient_objects`, `gradient_rows` and `gradients` hold the `gradientObjects` entries, the rows of the objects filled or stroked + // with a gradient, in walk order, and the gradients they use. + let mut gradient_objects: Vec = vec![]; + let mut gradient_rows: Vec<(String, String)> = vec![]; + let mut gradients = BTreeSet::new(); // The objects the walk has visited: a selection can hold a group and objects inside it, and each object counts once. // The set holds each object's address, so two objects that share an id (as a damaged file can hold) count as two. let mut seen: HashSet<*const Node> = HashSet::new(); @@ -412,27 +433,17 @@ fn info(s: &mut Session, p: &Value) -> Result { if let Some(k) = k { *counts.entry(k).or_default() += 1; } - // The paint kinds this object's fills and strokes use, each once. - let mut kinds = BTreeSet::new(); - // The patterns this object's fills and strokes use, each once. - let mut used_patterns: Vec<&str> = vec![]; - let painted_runs = painted_runs(d, n); - for pattern in own_patterns(d, n, &members, painted_runs.as_ref()) { - patterns.insert(pattern.to_string()); - used_patterns.push(pattern); - } for item in &n.appearance.items { - let paint = item.paint(); - // Objects counts an object with a pattern when Pattern Objects lists it, below. - if let Paint::Gradient(_) = paint { - kinds.insert("gradients"); - } - if let Paint::Solid { swatch: Some(name), .. } = paint + if let Paint::Solid { swatch: Some(name), .. } = item.paint() && d.swatch(name).is_some_and(|s| s.spot) { spot.insert(name.clone()); } } + // The patterns and the gradients this object's fills and strokes paint, each once. + let painted_runs = painted_runs(d, n); + let mut used_patterns = own_paints(d, n, &members, painted_runs.as_ref(), pattern_label); + let mut used_gradients = own_paints(d, n, &members, painted_runs.as_ref(), gradient_label); // The characters' own paints: an inline graphic draws no outline and does not take its // run's paint (a list marker set in its run does), and a stroke with no width paints // nothing. @@ -440,23 +451,32 @@ fn info(s: &mut Session, p: &Value) -> Result { for run in painted.iter().filter_map(|i| t.runs.get(*i)) { let stroke = (run.style.stroke_width > 0.0).then_some(&run.style.stroke); for paint in std::iter::once(&run.style.fill).chain(stroke) { - if let Paint::Pattern { pattern, .. } = paint { - patterns.insert(pattern.clone()); - if !used_patterns.contains(&pattern.as_str()) { - used_patterns.push(pattern); + for (used, label) in + [(&mut used_patterns, pattern_label as fn(&Paint) -> Option), (&mut used_gradients, gradient_label)] + { + if let Some(name) = label(paint) + && !used.contains(&name) + { + used.push(name); } } } } } + // Objects counts the objects that Pattern Objects and Gradient Objects list. if !used_patterns.is_empty() { let name = n.display_name(); + patterns.extend(used_patterns.iter().cloned()); + pattern_rows.push((name.clone(), used_patterns.join(", "))); pattern_objects.push(json!({ "id": n.id.0, "name": name, "patterns": used_patterns })); - pattern_rows.push((name, used_patterns.join(", "))); - kinds.insert("patterns"); + *counts.entry("patterns").or_default() += 1; } - for k in kinds { - *counts.entry(k).or_default() += 1; + if !used_gradients.is_empty() { + let name = n.display_name(); + gradients.extend(used_gradients.iter().cloned()); + gradient_rows.push((name.clone(), used_gradients.join(", "))); + gradient_objects.push(json!({ "id": n.id.0, "name": name, "gradients": used_gradients })); + *counts.entry("gradients").or_default() += 1; } if n.extras().mask.is_some() { *counts.entry("opacityMasks").or_default() += 1; @@ -506,10 +526,12 @@ fn info(s: &mut Session, p: &Value) -> Result { "patterns": d.patterns.len(), "patternNames": patterns, "patternObjects": pattern_objects, + "gradientNames": gradients, + "gradientObjects": gradient_objects, }); // The document's name: its file's, else its title. out["document"]["name"] = json!(st.title()); - let sections = sections(d, &out, &fonts, &image_rows, &repeat_rows, &pattern_rows, category); + let sections = sections(d, &out, &fonts, &image_rows, &repeat_rows, &pattern_rows, &gradient_rows, category); if text { return Ok(json!({ "text": report_text(&st.title(), selection_only, §ions) })); } @@ -583,12 +605,14 @@ mod tests { &json!({"gradient": {"kind": "linear", "stops": [{"offset": 0, "color": "#000"}, {"offset": 1, "color": "#fff"}]}}), ) .unwrap(); - s.execute("text.create", &json!({"x": 10, "y": 40, "text": "Hi"})).unwrap(); + let t = s.execute("text.create", &json!({"x": 10, "y": 40, "text": "Hi"})).unwrap()["id"].clone(); let i = s.execute("document.info", &json!({})).unwrap(); + // The type's characters take the current gradient fill, so it is a gradient object too. assert_eq!( (i["objects"]["paths"].as_u64(), i["objects"]["textObjects"].as_u64(), i["objects"]["gradients"].as_u64()), - (Some(1), Some(1), Some(1)) + (Some(1), Some(1), Some(2)) ); + assert_eq!((i["gradientObjects"][0]["id"].clone(), i["gradientObjects"][1]["id"].clone()), (r.clone(), t)); assert_eq!(i["fonts"][0], "Source Sans 3 Regular"); assert_eq!(i["document"]["artboards"][0]["width"], 200.0); s.execute("select.set", &json!({"ids": [r]})).unwrap(); @@ -842,6 +866,124 @@ mod tests { assert!(!text.contains("Unused"), "{text}"); } + /// Document Info › Gradient Objects lists each object filled or stroked with a gradient once, + /// with the gradients it uses (a linked swatch by name, else the gradient's kind), one in a + /// group and a type object whose characters use a gradient included; a gradient swatch no art + /// uses adds no row, paint that draws nothing is left out by the rules Pattern Objects follows, + /// and Objects counts the listed objects. With Selection Only it lists the objects in the + /// selection, each once, and the report writes the same rows. + #[test] + fn info_lists_the_gradient_objects() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap(); + let square = + |s: &mut Session, x: f64| s.execute("shape.rectangle", &json!({"x": x, "y": 0, "width": 10, "height": 10})).unwrap()["id"].clone(); + let ramp = |kind: &str| json!({"kind": kind, "stops": [{"offset": 0, "color": "#000"}, {"offset": 1, "color": "#fff"}]}); + s.execute("swatch.new", &json!({"name": "Ramp", "gradient": ramp("linear")})).unwrap(); + s.execute("swatch.new", &json!({"name": "Spare", "gradient": ramp("radial")})).unwrap(); + let (a, b, c, f) = (square(&mut s, 100.0), square(&mut s, 200.0), square(&mut s, 300.0), square(&mut s, 0.0)); + s.execute("paint.setFill", &json!({"ids": [a], "swatch": "Ramp"})).unwrap(); + s.execute("paint.setStroke", &json!({"ids": [a], "gradient": ramp("linear")})).unwrap(); + s.execute("layer.setProps", &json!({"id": a, "name": "Both"})).unwrap(); + s.execute("paint.setStroke", &json!({"ids": [b], "gradient": ramp("radial")})).unwrap(); + s.execute("paint.setFill", &json!({"ids": [f], "gradient": ramp("freeform")})).unwrap(); + s.execute("select.set", &json!({"ids": [b, c]})).unwrap(); + let group = s.execute("object.group", &json!({})).unwrap()["id"].clone(); + // A type object whose characters are filled with the gradient. + let t = s.execute("text.create", &json!({"x": 0, "y": 200, "text": "Hi"})).unwrap()["id"].clone(); + s.execute("paint.setFill", &json!({"ids": [t], "swatch": "Ramp"})).unwrap(); + s.execute("layer.setProps", &json!({"id": t, "name": "Type"})).unwrap(); + // A hidden gradient fill and a gradient stroke with no width paint nothing, so neither object is listed. + let (d, e) = (square(&mut s, 100.0), square(&mut s, 200.0)); + s.execute("paint.setStroke", &json!({"ids": [d], "none": true})).unwrap(); + s.execute("paint.setFill", &json!({"ids": [d], "swatch": "Ramp"})).unwrap(); + s.execute("appearance.setItem", &json!({"ids": [d], "index": 0, "visible": false})).unwrap(); + s.execute("paint.setFill", &json!({"ids": [e], "none": true})).unwrap(); + s.execute("paint.setStroke", &json!({"ids": [e], "gradient": ramp("linear")})).unwrap(); + s.execute("appearance.setItem", &json!({"ids": [e], "index": 1, "weight": 0})).unwrap(); + // Type with no characters, or of only spaces and a line break, paints nothing, and neither + // does a guide made from a path with a gradient. + let empty = s.execute("text.create", &json!({"x": 0, "y": 300, "text": ""})).unwrap()["id"].clone(); + s.execute("paint.setFill", &json!({"ids": [empty], "swatch": "Ramp"})).unwrap(); + s.execute("appearance.addFill", &json!({"ids": [empty]})).unwrap(); + let blank = s.execute("text.create", &json!({"x": 0, "y": 320, "text": " \n"})).unwrap()["id"].clone(); + s.execute("paint.setFill", &json!({"ids": [blank], "swatch": "Ramp"})).unwrap(); + let guide = square(&mut s, 300.0); + s.execute("paint.setFill", &json!({"ids": [guide], "swatch": "Ramp"})).unwrap(); + s.execute("select.set", &json!({"ids": [guide]})).unwrap(); + s.execute("view.guides.make", &json!({})).unwrap(); + // Nor does a compound path's member's own gradient fill, or an image's gradient fill + // without an effect that gives it a shape. + let (m1, m2) = (square(&mut s, 320.0), square(&mut s, 340.0)); + s.execute("select.set", &json!({"ids": [m1, m2]})).unwrap(); + let compound = s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].clone(); + s.execute("paint.setFill", &json!({"ids": [compound], "color": "#000000"})).unwrap(); + s.execute("paint.setStroke", &json!({"ids": [compound], "none": true})).unwrap(); + let node = |s: &Session, id: &Value| s.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap().clone(); + let (member, paint) = (node(&s, &compound).children().unwrap()[0].id, node(&s, &a).appearance); + let pic = square(&mut s, 360.0); + s.execute("select.set", &json!({"ids": [pic]})).unwrap(); + s.execute("object.rasterize", &json!({"ppi": 72})).unwrap(); + let image = s.doc().unwrap().selection.objects[0]; + s.edit("own paint", |d, _| { + d.node_mut(member).unwrap().appearance = paint.clone(); + d.node_mut(image).unwrap().appearance = paint; + Ok(()) + }) + .unwrap(); + // Underlined spaces draw a bar in the gradient, so that type object is listed. + let bar = s.execute("text.create", &json!({"x": 0, "y": 340, "text": " "})).unwrap()["id"].clone(); + s.execute("paint.setFill", &json!({"ids": [bar], "swatch": "Ramp"})).unwrap(); + s.execute("select.set", &json!({"ids": [bar]})).unwrap(); + s.execute("text.setFormat", &json!({"underline": true})).unwrap(); + s.execute("layer.setProps", &json!({"id": bar, "name": "Bar"})).unwrap(); + let i = s.execute("document.info", &json!({})).unwrap(); + assert_eq!( + i["gradientObjects"], + json!([ + {"id": a, "name": "Both", "gradients": ["Ramp", "Linear gradient"]}, + {"id": b, "name": "", "gradients": ["Radial gradient"]}, + {"id": f, "name": "", "gradients": ["Freeform gradient"]}, + {"id": t, "name": "Type", "gradients": ["Ramp"]}, + {"id": bar, "name": "Bar", "gradients": ["Ramp"]}, + ]) + ); + assert_eq!(i["gradientNames"], json!(["Freeform gradient", "Linear gradient", "Radial gradient", "Ramp"])); + // Objects counts the objects that Gradient Objects lists. + assert_eq!(i["objects"]["gradients"].as_u64(), Some(5), "{}", i["objects"]); + s.execute("select.set", &json!({"ids": [d, e, empty, blank, guide, compound, image.0]})).unwrap(); + let i = s.execute("document.info", &json!({"selectionOnly": true})).unwrap(); + assert!(i["objects"]["gradients"].is_null() && i["gradientObjects"] == json!([]), "{}", i["objects"]); + let i = s.execute("document.info", &json!({"category": "gradients"})).unwrap(); + assert_eq!((i["sections"][0]["id"].as_str(), i["sections"][0]["title"].as_str()), (Some("gradients"), Some("Gradient Objects"))); + assert_eq!( + i["sections"][0]["rows"], + json!([ + ["Both", "Ramp, Linear gradient"], + ["", "Radial gradient"], + ["", "Freeform gradient"], + ["Type", "Ramp"], + ["Bar", "Ramp"] + ]) + ); + // A selection that holds the group and the object inside it lists that object once. + for ids in [json!([group, b]), json!([b, group])] { + s.execute("select.set", &json!({"ids": ids})).unwrap(); + let i = s.execute("document.info", &json!({"selectionOnly": true, "category": "gradients"})).unwrap(); + assert_eq!(i["sections"][0]["rows"], json!([["", "Radial gradient"]]), "{ids}"); + assert_eq!(i["objects"]["gradients"].as_u64(), Some(1), "{ids}"); + } + s.execute("select.set", &json!({"ids": [t]})).unwrap(); + let i = s.execute("document.info", &json!({"selectionOnly": true, "category": "gradients"})).unwrap(); + assert_eq!((i["sections"][0]["rows"].clone(), i["gradientNames"].clone()), (json!([["Type", "Ramp"]]), json!(["Ramp"]))); + let text = s.execute("document.info", &json!({"category": "gradients", "format": "text"})).unwrap()["text"].as_str().unwrap().to_string(); + assert!( + text.contains("\nGRADIENT OBJECTS\nBoth: Ramp, Linear gradient\n: Radial gradient\n: Freeform gradient\nType: Ramp\nBar: Ramp\n"), + "{text}" + ); + assert!(!text.contains("Spare"), "{text}"); + } + /// Document Info › Objects counts an object with a gradient or a pattern on both its fill and /// its stroke once, and with Selection Only a selection that holds a group and an object inside /// it counts that object once. diff --git a/crates/engine/src/tests/package.rs b/crates/engine/src/tests/package.rs index e15258bec6..aeecc9fcc2 100644 --- a/crates/engine/src/tests/package.rs +++ b/crates/engine/src/tests/package.rs @@ -393,7 +393,7 @@ fn the_document_info_report_has_every_category() { "BRUSHES", "SPOT COLORS", "PATTERN OBJECTS", - "GRADIENT SWATCHES", + "GRADIENT OBJECTS", "SYMBOLS", "FONTS", "FONT DETAILS", diff --git a/crates/ui-egui/src/panels/doc_info.rs b/crates/ui-egui/src/panels/doc_info.rs index f6becb3911..4d6bf05ec1 100644 --- a/crates/ui-egui/src/panels/doc_info.rs +++ b/crates/ui-egui/src/panels/doc_info.rs @@ -1,6 +1,6 @@ //! Document Info panel: the document's setup and what it contains (objects by kind, graphic -//! styles, brushes, spot colors, patterns, gradients, symbols, fonts and their files, linked and embedded -//! images, repeat objects), for the whole document or the selection only. The flyout picks one category (or all) +//! styles, brushes, spot colors, objects painted with patterns and with gradients, symbols, fonts and their files, +//! linked and embedded images, repeat objects), for the whole document or the selection only. The flyout picks one category (or all) //! and saves the text report (`docInfo.save`, the report File → Package writes too). use egui::Ui; @@ -191,9 +191,20 @@ mod tests { menu_text.contains("Font Details") && menu_text.contains("Brushes") && menu_text.contains("Repeat Objects") + && menu_text.contains("Gradient Objects") && menu_text.contains("Save…"), "{menu_text}" ); + // Gradient Objects lists the objects painted with a gradient by name. + let r = app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap()["id"].clone(); + let ramp = json!({"kind": "radial", "stops": [{"offset": 0, "color": "#000"}, {"offset": 1, "color": "#fff"}]}); + app.session.execute("paint.setFill", &json!({"ids": [r], "gradient": ramp})).unwrap(); + app.session.execute("layer.setProps", &json!({"id": r, "name": "Glow"})).unwrap(); + let gradients = crate::tests_labels::painted_text(&mut app, |app, ui| { + set_pstate(ui.ctx(), "docinfo-category", "gradients".to_string()); + show(app, ui); + }); + assert!(gradients.contains("Gradient Objects (1)") && gradients.contains("Glow") && gradients.contains("Radial gradient"), "{gradients}"); let filtered = crate::tests_labels::painted_text(&mut app, |app, ui| { set_pstate(ui.ctx(), "docinfo-category", "fonts".to_string()); show(app, ui); @@ -210,6 +221,7 @@ mod tests { assert_eq!(r["path"], "/tmp/info.txt"); let report = String::from_utf8(written.borrow().clone()).unwrap(); assert!(report.starts_with("Document Info: ") && report.contains("\nFONT DETAILS\n") && report.contains("\nREPEAT OBJECTS\n"), "{report}"); + assert!(report.contains("\nGRADIENT OBJECTS\nGlow: Radial gradient\n"), "{report}"); } /// The report the panel menu's Save… writes with category `category` shown ("" for All diff --git a/docs/mcp.md b/docs/mcp.md index 5826c5c8c4..9604427327 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5751,7 +5751,11 @@ whose drawn characters use a pattern included, hidden fills and strokes, strokes member's own fills and strokes, an image's fills and strokes that no Convert to Shape or Outline Object effect gives a shape, and characters that draw nothing (spaces and line breaks without an underline or strikethrough) left out, with the patterns it uses, which the patterns category lists as Pattern Objects and `objects.patterns` counts; with `selectionOnly`, those in the selection), -`patternNames` (the patterns those objects use) and `pathLength` (pt: the total length of the paths, compound paths' pieces included and guides left +`patternNames` (the patterns those objects use), `gradientObjects` (`[{id, name, gradients}]`: every object filled or +stroked with a gradient, by the same rules as `patternObjects`, with the gradients it uses: the name of the gradient +swatch a gradient is linked to, else `Linear gradient`, `Radial gradient` or `Freeform gradient`; the gradients category +lists them as Gradient Objects and `objects.gradients` counts them; gradient swatches no art uses are not listed; with +`selectionOnly`, those in the selection), `gradientNames` (the gradients those objects use) and `pathLength` (pt: the total length of the paths, compound paths' pieces included and guides left out, which objects lists as Path Length in the document's units, say for a cut line or an LED strip along the letters); `format: "text"` returns the plain-text report Document Info › Save… (`docInfo.save {path?, selectionOnly?, category?}`: only `category` when given, else every category) and the package report write. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index b4c949f742..fdaf038e16 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1310,8 +1310,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Document Info has a Brushes category after Graphic Styles. It lists the Brushes panel's brushes, or with Selection Only it lists the brushes that the selected objects' strokes use. The Document Info report and Package's report list them too (`document.info {category: "brushes"}`, `brushNames`). Document Info has a Repeat Objects category after Embedded Images. It lists each radial, grid and mirror Repeat object by name with its kind and the number of instances it makes. For a grid it gives the rows and columns instead, followed by the number of instances it draws when that is fewer. With Selection Only it lists the Repeat objects in the selection. The Document Info report and Package's report list them too (`document.info {category: "repeats"}`, `repeatObjects`). Document Info's Pattern Objects category lists the objects whose fill or stroke is a pattern, the category the [Document Info panel](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/document-info-panel.html) page describes, each by name with the patterns it uses. With Selection Only it lists those in the selection, and the Document Info report and Package's report list them too (`document.info {category: "patterns"}`, `patternObjects`). Listing an object with a pattern fill and a pattern stroke once, listing objects inside groups and type objects whose characters use a pattern, and leaving out hidden fills and strokes, strokes with no width, guides, a compound path member's own fills and strokes, an image's fills and strokes without a shape, characters the type does not draw and patterns no art uses, and counting the listed objects in Objects, are VectorCraft's own rules. Test: `cmd::docinfo::tests::info_lists_the_pattern_objects` (vectorcraft-engine). + Document Info's Gradient Objects category lists the objects whose fill or stroke is a gradient, the category the [Document Info panel](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/document-info-panel.html) page describes as the count of objects with gradient fills or strokes. Each object is listed by name with the gradients it uses. With Selection Only it lists those in the selection, and the Document Info report and Package's report list them too (`document.info {category: "gradients"}`, `gradientObjects`). Naming a linked gradient by its swatch and any other gradient by its kind, and leaving out paint that draws nothing by the rules Pattern Objects follows, gradient swatches no art uses, gradient meshes and gradients drawn by effects, are VectorCraft's own rules. Objects counts the listed objects as Gradient Objects. Test: `cmd::docinfo::tests::info_lists_the_gradient_objects` (vectorcraft-engine). Document Info's Save… writes the category the panel shows, with Selection Only applied, as the [Document Info panel](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/document-info-panel.html) page describes ("Exports the selected category's visible information to a text file"). With All Categories shown it writes every category, which is VectorCraft's own rule (`docInfo.save {category}`). Test: `panels::doc_info::tests::save_writes_the_category_the_panel_shows`. - Document Info's Save… writes the category the panel shows, with Selection Only applied, as the [Document Info panel](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/document-info-panel.html) page describes. With All Categories shown it writes every category, which is VectorCraft's own rule (`docInfo.save {category}`). Test: `panels::doc_info::tests::save_writes_the_category_the_panel_shows`. The Brushes panel menu's Select All Unused selects the brushes that are not used in the document, as the [Brushes](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html) page (archived copy) describes. A brush counts as used when a stroke paints with it in the art (opacity masks included), in symbols, graph designs, pattern tiles or graphic styles, or in the art of a used brush (`brush.unused`). A rename in Brush Options and Delete Brush change every stroke that paints with the brush: in the layers and opacity masks, symbols, graph designs, pattern tiles, graphic styles and the art of other brushes. A rename gives those strokes the new name, and Delete Brush makes them plain strokes, in one undo step. In Pattern Brush Options, a menu below the Outer Corner Tile and the Inner Corner Tile sets each to the brush's own corner art (Original), None, or a tile made from the side tile: Auto-Centered stretches one side tile around the corner, Auto-Between bends a side tile on each side into the corner's diagonal, Auto-Sliced cuts a side tile on each side along the diagonal, and Auto-Overlap runs a side tile on each side past the corner. Auto-Sliced masks the tiles at the diagonal, so their outlines stay as drawn. Brushes saved without these choices draw their corners as before (`brush.options` `outer_source`, `inner_source`). Evidence: `tests::pattern_auto_corners_fill_each_corner_of_a_square`, `tests::pattern_corner_source_none_leaves_that_kind_of_corner_empty` (vectorcraft-brush), `dialogs::brush_options::tests::pattern_options_set_each_corner_tile_to_original_none_or_an_automatic_tile` (vectorcraft-ui-egui).