diff --git a/crates/cad/src/lib.rs b/crates/cad/src/lib.rs index d459f95b40..bb752267aa 100644 --- a/crates/cad/src/lib.rs +++ b/crates/cad/src/lib.rs @@ -429,7 +429,8 @@ pub fn export(doc: &Document, opts: &DxfOptions) -> Result { return Err("the export region is not finite".into()); } // Symbols' art is placed without clipping, as the scene places it. - let baked = vectorcraft_effects::bake_document_placing_symbols(doc, false); + let baked = + vectorcraft_effects::bake_document_placing_symbols(doc, false, vectorcraft_effects::WrittenLayers { hidden: true, non_printing: true }); let doc = baked.as_ref().unwrap_or(doc); Ok(scene::Scene::new(doc, opts).run()) } diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs index 0ce8fb8aef..12f967922d 100644 --- a/crates/effects/src/bake.rs +++ b/crates/effects/src/bake.rs @@ -124,6 +124,9 @@ thread_local! { /// How deep the object [`bake_node`] is baking sits in the tree it returns: a top-level layer /// is 1 deep. static LEVEL: std::cell::Cell = const { std::cell::Cell::new(1) }; + /// Whether [`bake_node`] is baking inside an object the export does not write ([`unwritten`]), + /// where no instance is placed. + static UNWRITTEN: std::cell::Cell = const { std::cell::Cell::new(false) }; } /// `f` run for an object one level deeper ([`LEVEL`]) than the one being baked. @@ -149,6 +152,30 @@ struct Placing { /// How deep the document's symbols and patterns draw, for the instances and pattern paints /// left in placed art. heights: vectorcraft_doc::depth::DrawnHeights, + /// The hidden layers and layers that do not print that the export writes. + layers: WrittenLayers, +} + +/// The layers an exporter writes besides the shown, printing ones +/// ([`bake_document_placing_symbols`]). Template layers are never written. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct WrittenLayers { + /// Hidden layers (written turned off, as PDF layers and DXF write them). + pub hidden: bool, + /// Layers that do not print. + pub non_printing: bool, +} + +/// Does the export leave `n` and everything in it out, while [`bake_document_placing_symbols`] +/// bakes? A hidden object other than a layer, a template layer, and a hidden layer or a layer +/// that does not print when the export leaves those out ([`WrittenLayers`]). +fn unwritten(n: &Node) -> bool { + PLACING.with(|p| { + p.borrow().as_ref().is_some_and(|p| match n.kind { + NodeKind::Layer { template, printable, .. } => template || !(printable || p.layers.non_printing) || !(n.visible || p.layers.hidden), + _ => !n.visible, + }) + }) } /// Can `baked`, the baked art of a placed instance, take the instance's place [`LEVEL`] deep, @@ -201,13 +228,14 @@ fn drawn_symbols(d: &Document) -> HashSet { } /// Does `n`'s subtree hold an instance of a symbol that [`bake_document_placing_symbols`] replaces -/// by its placed art? False outside it. +/// by its placed art? False outside it, and for objects the export does not write ([`unwritten`]). fn holds_placed_symbol(n: &Node) -> bool { fn walk(n: &Node, symbols: &HashSet) -> bool { - matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if symbols.contains(symbol)) - || n.children().is_some_and(|ch| ch.iter().any(|c| walk(c, symbols))) + !unwritten(n) + && (matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if symbols.contains(symbol)) + || n.children().is_some_and(|ch| ch.iter().any(|c| walk(c, symbols)))) } - PLACING.with(|p| p.borrow().as_ref().is_some_and(|p| walk(n, &p.symbols))) + !UNWRITTEN.with(|c| c.get()) && PLACING.with(|p| p.borrow().as_ref().is_some_and(|p| walk(n, &p.symbols))) } /// Symbol instance `n`, while [`bake_document_placing_symbols`] bakes and its symbol is one it @@ -216,14 +244,15 @@ fn holds_placed_symbol(n: &Node) -> bool { /// place it, in a group with the instance's id, name, visibility, transparency, opacity mask and /// raster effects. The art is placed before its effects are baked, as the canvas draws it, so an /// instance that scales its art does not scale what its effects do. `None` for other objects, -/// for instances nested [`vectorcraft_doc::depth::MAX_DEPTH`] deep in such art, and when the art +/// for instances the export does not write ([`unwritten`], hidden or inside such an object), for +/// instances nested [`vectorcraft_doc::depth::MAX_DEPTH`] deep in such art, and when the art /// as placed, before its effects are baked, would already take the art placed for the export /// past [`Placing::most_nodes`]; those instances stay as they are. [`bake_node`] checks the baked /// art with [`placed_fits`]. fn placed_symbol_art(d: &Document, n: &Node) -> Option { let NodeKind::SymbolInstance { symbol, xf } = &n.kind else { return None }; let (clip, most_nodes) = PLACING.with(|p| p.borrow().as_ref().filter(|p| p.symbols.contains(symbol)).map(|p| (p.clip_slices, p.most_nodes)))?; - if INSTANCE_DEPTH.with(|c| c.get()) >= vectorcraft_doc::depth::MAX_DEPTH { + if INSTANCE_DEPTH.with(|c| c.get()) >= vectorcraft_doc::depth::MAX_DEPTH || UNWRITTEN.with(|c| c.get()) || unwritten(n) { return None; } let sym = d.symbols.iter().find(|s| s.name == *symbol)?; @@ -242,6 +271,13 @@ fn placed_symbol_art(d: &Document, n: &Node) -> Option { /// `n` with its geometry effects evaluated (`None` = unchanged). fn bake_node(d: &mut Document, n: &Node) -> Option { + // Inside an object the export leaves out, effects are baked but no instance is placed. + if !UNWRITTEN.with(|c| c.get()) && unwritten(n) { + UNWRITTEN.with(|c| c.set(true)); + let out = bake_node(d, n); + UNWRITTEN.with(|c| c.set(false)); + return out; + } // A cancelled export leaves the rest of the document as it is: the caller drops the result. // A ruler guide draws nothing and stays as it is, last in its layer. if n.is_ruler_guide() || !(needs_bake(n) || holds_placed_symbol(n)) || vectorcraft_doc::cancel::is_cancelled() { @@ -848,31 +884,47 @@ pub fn bake_document(doc: &Document) -> Option { /// exporters that write each instance's art in its place (PDF, print, EPS, DXF), rather than /// writing each symbol once. `clip_slices`: whether 9-slice cells are clipped /// ([`Document::place_symbol_art`]) or not ([`Document::place_symbol_art_unclipped`]), as the -/// exporter places other instances. The symbols' own art is left as it is. -pub fn bake_document_placing_symbols(doc: &Document, clip_slices: bool) -> Option { - bake(doc, Some((clip_slices, vectorcraft_doc::MAX_SYMBOL_NODES))) +/// exporter places other instances. `layers`: the hidden layers and layers that do not print +/// that the exporter writes. Instances the export does not write (hidden ones, those in hidden +/// objects, on template layers, and on the layers `layers` leaves out) stay as they are. The +/// symbols' own art is left as it is. +pub fn bake_document_placing_symbols(doc: &Document, clip_slices: bool, layers: WrittenLayers) -> Option { + bake(doc, Some((clip_slices, vectorcraft_doc::MAX_SYMBOL_NODES, layers))) } /// Restores what [`bake`] changes in the thread's baking state when it ends. -struct Restore(Option>, u64, usize); +struct Restore(Option>, u64, usize, bool); impl Drop for Restore { fn drop(&mut self) { PLACING.with(|p| *p.borrow_mut() = self.0.take()); PLACED_NODES.with(|c| c.set(self.1)); LEVEL.with(|c| c.set(self.2)); + UNWRITTEN.with(|c| c.set(self.3)); } } -/// [`bake_document`], or with `placing` [`bake_document_placing_symbols`] with that `clip_slices` -/// and [`Placing::most_nodes`]. -fn bake(doc: &Document, placing: Option<(bool, u64)>) -> Option { +/// [`bake_document`], or with `placing` [`bake_document_placing_symbols`] with that `clip_slices`, +/// [`Placing::most_nodes`] and [`Placing::layers`]. +fn bake(doc: &Document, placing: Option<(bool, u64, WrittenLayers)>) -> Option { let placed = doc.with_placed_art(); - let placing = placing.map(|(clip_slices, most_nodes)| (drawn_symbols(&placed), clip_slices, most_nodes)).filter(|p| !p.0.is_empty()).map( - |(symbols, clip_slices, most_nodes)| Placing { symbols, clip_slices, most_nodes, heights: vectorcraft_doc::depth::DrawnHeights::of(&placed) }, - ); + let placing = placing + .map(|(clip_slices, most_nodes, layers)| (drawn_symbols(&placed), clip_slices, most_nodes, layers)) + .filter(|p| !p.0.is_empty()) + .map(|(symbols, clip_slices, most_nodes, layers)| Placing { + symbols, + clip_slices, + most_nodes, + heights: vectorcraft_doc::depth::DrawnHeights::of(&placed), + layers, + }); // A bake inside another one (of art an effect makes) places symbols only when it asks to. - let _restore = Restore(PLACING.with(|p| p.replace(placing.map(Rc::new))), PLACED_NODES.with(|c| c.replace(0)), LEVEL.with(|c| c.replace(1))); + let _restore = Restore( + PLACING.with(|p| p.replace(placing.map(Rc::new))), + PLACED_NODES.with(|c| c.replace(0)), + LEVEL.with(|c| c.replace(1)), + UNWRITTEN.with(|c| c.replace(false)), + ); if !placed.layers.iter().any(|l| needs_bake(l) || holds_placed_symbol(l)) { return match placed { std::borrow::Cow::Owned(d) => Some(d), diff --git a/crates/effects/src/bake/tests.rs b/crates/effects/src/bake/tests.rs index be72b4df6e..ed7699651d 100644 --- a/crates/effects/src/bake/tests.rs +++ b/crates/effects/src/bake/tests.rs @@ -1100,7 +1100,7 @@ fn placing_symbols_bakes_their_art_where_each_instance_places_it() { d.insert(Some(layer), 0, instance(901, "S", Affine::translate((0.0, 100.0)) * Affine::scale(2.0))).unwrap(); add_symbol(&mut d, "S", moved_square(800)); assert!(bake_document(&d).is_none(), "nothing in the layers needs baking"); - let out = bake_document_placing_symbols(&d, true).unwrap(); + let out = bake_document_placing_symbols(&d, true, ALL).unwrap(); assert!(matches!(out.node(NodeId(900)).unwrap().kind, NodeKind::Group { .. })); assert!(near_rect(bounds(&out, 900), Rect::new(110.0, 40.0, 120.0, 50.0)), "{:?}", bounds(&out, 900)); assert!(near_rect(bounds(&out, 901), Rect::new(120.0, 180.0, 140.0, 200.0)), "{:?}", bounds(&out, 901)); @@ -1115,7 +1115,7 @@ fn placing_symbols_reaches_the_art_of_nested_instances() { let mut d = doc_with(instance(900, "Outer", Affine::IDENTITY)); add_symbol(&mut d, "Inner", moved_square(800)); add_symbol(&mut d, "Outer", Node::group(NodeId(801), vec![Arc::new(instance(802, "Inner", Affine::translate((0.0, 100.0))))])); - let out = bake_document_placing_symbols(&d, true).unwrap(); + let out = bake_document_placing_symbols(&d, true, ALL).unwrap(); assert!(near_rect(bounds(&out, 900), Rect::new(110.0, 140.0, 120.0, 150.0)), "{:?}", bounds(&out, 900)); } @@ -1130,7 +1130,7 @@ fn placing_a_symbol_that_holds_itself_stops() { "Loop", Node::group(NodeId(801), vec![Arc::new(moved_square(800)), Arc::new(instance(802, "Loop", Affine::translate((0.0, 1.0))))]), ); - let out = bake_document_placing_symbols(&d, true).unwrap(); + let out = bake_document_placing_symbols(&d, true, ALL).unwrap(); let mut lefts = vec![]; out.node(NodeId(900)).unwrap().walk(&mut |n| lefts.extend(n.path_data().and_then(|p| p.bounds()).map(|b| b.x0))); assert!(lefts.len() > 1 && lefts.iter().all(|x| (x - 110.0).abs() < 1e-6), "{lefts:?}"); @@ -1160,7 +1160,7 @@ fn placing_symbols_keeps_dynamic_instances_changes() { let mut d = doc_with(inst); add_symbol(&mut d, "D", Node::group(NodeId(802), vec![Arc::new(square), Arc::new(sibling)])); d.symbols[0].dynamic = true; - let out = bake_document_placing_symbols(&d, true).unwrap(); + let out = bake_document_placing_symbols(&d, true, ALL).unwrap(); let mut fills = vec![]; out.node(NodeId(900)).unwrap().walk(&mut |n| { if n.path_data().is_some() @@ -1197,7 +1197,7 @@ fn placing_symbols_keeps_the_instances_stain() { art.appearance.effects.push(fx(id, params)); let mut d = doc_with(inst.clone()); add_symbol(&mut d, "Mark", art); - let out = bake_document_placing_symbols(&d, true).unwrap(); + let out = bake_document_placing_symbols(&d, true, ALL).unwrap(); assert!(!is_instance(&out, 900), "{id}: the instance is placed"); let mut fills = vec![]; out.node(NodeId(900)).unwrap().walk(&mut |n| { @@ -1209,6 +1209,9 @@ fn placing_symbols_keeps_the_instances_stain() { } } +/// Every layer written, as DXF writes them (template layers are never written). +const ALL: WrittenLayers = WrittenLayers { hidden: true, non_printing: true }; + fn is_instance(d: &Document, id: u64) -> bool { d.node(NodeId(id)).is_some_and(|n| matches!(n.kind, NodeKind::SymbolInstance { .. })) } @@ -1223,13 +1226,13 @@ fn placing_symbols_counts_the_baked_art() { let layer = d.layers[0].id; d.insert(Some(layer), 0, instance(901, "S", Affine::translate((0.0, 100.0)))).unwrap(); add_symbol(&mut d, "S", gradient_copies(800)); - let all = bake(&d, Some((true, u64::MAX))).unwrap(); + let all = bake(&d, Some((true, u64::MAX, ALL))).unwrap(); let each = all.node(NodeId(900)).map_or(0, |n| n.count_with_masks() as u64); assert!(each > 2, "the instance's group, and the copies as objects of their own: {each}"); assert_eq!(all.node(NodeId(901)).map(|n| n.count_with_masks() as u64), Some(each)); - let fits = bake(&d, Some((true, 2 * each))).unwrap(); + let fits = bake(&d, Some((true, 2 * each, ALL))).unwrap(); assert!(!is_instance(&fits, 900) && !is_instance(&fits, 901)); - let short = bake(&d, Some((true, 2 * each - 1))).unwrap(); + let short = bake(&d, Some((true, 2 * each - 1, ALL))).unwrap(); assert_eq!(usize::from(is_instance(&short, 900)) + usize::from(is_instance(&short, 901)), 1, "one instance stays as it is"); } @@ -1241,14 +1244,60 @@ fn placing_symbols_counts_nested_art_once() { let mut d = doc_with(instance(900, "Outer", Affine::IDENTITY)); add_symbol(&mut d, "Inner", gradient_copies(800)); add_symbol(&mut d, "Outer", Node::group(NodeId(801), vec![Arc::new(instance(802, "Inner", Affine::IDENTITY))])); - let all = bake(&d, Some((true, u64::MAX))).unwrap(); + let all = bake(&d, Some((true, u64::MAX, ALL))).unwrap(); let total = all.node(NodeId(900)).map_or(0, |n| n.count_with_masks() as u64); - let exact = bake(&d, Some((true, total))).unwrap(); + let exact = bake(&d, Some((true, total, ALL))).unwrap(); assert!(!is_instance(&exact, 900) && !is_instance(&exact, 802), "both are placed within {total}"); - let short = bake(&d, Some((true, total - 1))).unwrap(); + let short = bake(&d, Some((true, total - 1, ALL))).unwrap(); assert!(is_instance(&short, 900), "the outer instance stays as it is"); } +/// Instances the export does not write are not placed and do not count against the budget: with +/// room for one instance's baked art, hidden instances, an instance in a hidden group, one on a +/// template layer, one on a layer that does not print and one on a hidden layer (when the export +/// leaves those layers out) come first and stay as they are, and the visible instance after them +/// is placed. An export that writes those layers places their instances first. +#[test] +fn placing_symbols_skips_instances_the_export_leaves_out() { + use vectorcraft_geom::Affine; + let mut d = doc_with(instance(900, "S", Affine::IDENTITY)); + add_symbol(&mut d, "S", gradient_copies(800)); + let each = bake(&d, Some((true, u64::MAX, ALL))).unwrap().node(NodeId(900)).map_or(0, |n| n.count_with_masks() as u64); + let layer = d.layers[0].id; + let mut hidden = instance(901, "S", Affine::IDENTITY); + hidden.visible = false; + let mut group = Node::group(NodeId(902), vec![Arc::new(instance(903, "S", Affine::IDENTITY))]); + group.visible = false; + for (i, n) in [hidden, group].into_iter().enumerate() { + d.insert(Some(layer), i, n).unwrap(); + } + let mut left_out = vec![]; + for (id, template, printable, visible) in [(910, true, true, true), (920, false, false, true), (930, false, true, false)] { + let mut l = Node::layer(NodeId(id), &format!("L{id}"), vectorcraft_doc::LayerColor::Preset(0)); + if let NodeKind::Layer { template: t, printable: p, .. } = &mut l.kind { + *t = template; + *p = printable; + } + l.visible = visible; + if let Some(ch) = l.children_mut() { + ch.push(Arc::new(instance(id + 1, "S", Affine::IDENTITY))); + } + left_out.push(Arc::new(l)); + } + d.layers.splice(0..0, left_out); + let out = bake(&d, Some((true, each, WrittenLayers::default()))).unwrap(); + assert!(!is_instance(&out, 900), "the visible instance is placed within {each}"); + for id in [901, 903, 911, 921, 931] { + assert!(is_instance(&out, id), "{id} stays as it is"); + } + // An export that writes the layers that do not print, or the hidden layers, places that + // layer's instance first; the template layer's instance is never placed. + for (layers, first) in [(WrittenLayers { non_printing: true, hidden: false }, 921), (WrittenLayers { non_printing: false, hidden: true }, 931)] { + let out = bake(&d, Some((true, each, layers))).unwrap(); + assert!(!is_instance(&out, first) && is_instance(&out, 900) && is_instance(&out, 911), "{layers:?}"); + } +} + /// `leaf` in `levels` groups, with ids from `id` up. fn in_groups(id: u64, levels: u64, leaf: Node) -> Node { (0..levels).fold(leaf, |n, i| Node::group(NodeId(id + i), vec![Arc::new(n)])) @@ -1270,7 +1319,7 @@ fn placing_symbols_keeps_the_baked_art_within_the_drawn_depth() { add_symbol(&mut d, "E", in_groups(5000, 1, gradient_copies(5100))); assert!(!d.too_deep(None)); assert_eq!(DrawnHeights::of(&d).deepest(&d.layers[0], 1), MAX_DRAWN_DEPTH); - let out = bake_document_placing_symbols(&d, true).unwrap(); + let out = bake_document_placing_symbols(&d, true, ALL).unwrap(); // E's square becomes a group of copies, a level past the cap: E's instance stays. assert!(is_instance(&out, 4100), "E's instance stays as it is"); assert!(!is_instance(&out, 900) && !is_instance(&out, 3100), "A to D are placed"); @@ -1278,5 +1327,5 @@ fn placing_symbols_keeps_the_baked_art_within_the_drawn_depth() { // Without the chain above it, E's art is placed. let mut shallow = doc_with(instance(900, "E", Affine::IDENTITY)); add_symbol(&mut shallow, "E", in_groups(5000, 1, gradient_copies(5100))); - assert!(!is_instance(&bake_document_placing_symbols(&shallow, true).unwrap(), 900)); + assert!(!is_instance(&bake_document_placing_symbols(&shallow, true, ALL).unwrap(), 900)); } diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 2b5d561021..3d74ac4fe0 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -69,7 +69,8 @@ pub use adjust::{ is_channel, }; pub use bake::{ - StrokeArt, bake_appearance, bake_document, bake_document_placing_symbols, bake_type_appearance, expand_art, expand_leaf, fresh_ids, needs_bake, + StrokeArt, WrittenLayers, bake_appearance, bake_document, bake_document_placing_symbols, bake_type_appearance, expand_art, expand_leaf, + fresh_ids, needs_bake, }; pub use clip::{WarpSeams, clip_outline, knockout_silhouette, warp_pieces_translucent}; pub use group::{ diff --git a/crates/eps/src/lib.rs b/crates/eps/src/lib.rs index 2dbadf4fea..534778286a 100644 --- a/crates/eps/src/lib.rs +++ b/crates/eps/src/lib.rs @@ -230,7 +230,8 @@ pub fn export(doc: &Document, opts: &EpsOptions, preview: Option<&Raster>, thumb } // Type in the document's composite fonts is drawn in them, whether or not an app has the document open. let _lent = vectorcraft_text::FontDb::global().lend_composite_fonts(&doc.composite_fonts); - let baked = vectorcraft_effects::bake_document_placing_symbols(doc, true); + let baked = + vectorcraft_effects::bake_document_placing_symbols(doc, true, vectorcraft_effects::WrittenLayers { hidden: false, non_printing: true }); let doc = baked.as_ref().unwrap_or(doc); let page = scene::Scene::new(doc, opts).run(); let mut setting_notes = vec![]; diff --git a/crates/pdf/src/export/entry.rs b/crates/pdf/src/export/entry.rs index 96dc97a6f8..6cf8677269 100644 --- a/crates/pdf/src/export/entry.rs +++ b/crates/pdf/src/export/entry.rs @@ -47,7 +47,13 @@ fn export_with_report_inner(doc: &Document, opts: &PdfOptions, left_out: &mut Ha vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?; // Live geometry effects export as their result, in symbols' art too; raster effects are // reported below. - let baked = vectorcraft_effects::bake_document_placing_symbols(doc, true); + // Only the layers this export writes place their instances' art (see `ex.non_printing` and + // `ex.layers` below). + let layers = vectorcraft_effects::WrittenLayers { + hidden: set.writes_layers() && !doc.page_knockout, + non_printing: set.include_non_printing || set.create_layers, + }; + let baked = vectorcraft_effects::bake_document_placing_symbols(doc, true, layers); // Baking stops early when cancelled, leaving a half-baked copy that must not be written. vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?; let doc = baked.as_ref().unwrap_or(doc); diff --git a/crates/pdf/src/print.rs b/crates/pdf/src/print.rs index 57d30e327d..2a3cbf98af 100644 --- a/crates/pdf/src/print.rs +++ b/crates/pdf/src/print.rs @@ -183,11 +183,14 @@ pub fn keep_layers(nodes: &mut Vec>, which: PrintLayers) { /// only the layers that print, overprints in composite output as [`PrintAdvanced::overprints`] /// says. pub fn printed_document<'a>(doc: &'a Document, set: &PrintSettings) -> Cow<'a, Document> { - let mut doc = vectorcraft_effects::bake_document_placing_symbols(doc, true).map_or(Cow::Borrowed(doc), Cow::Owned); - let d = doc.to_mut(); - keep_layers(&mut d.layers, set.print_layers); - composite_overprints(d, set); - doc + // The layers that do not print are left out first, so the art placed for symbols is only + // the art that prints; the layers kept are shown. + let mut kept = doc.clone(); + keep_layers(&mut kept.layers, set.print_layers); + let layers = vectorcraft_effects::WrittenLayers { hidden: false, non_printing: true }; + let mut d = vectorcraft_effects::bake_document_placing_symbols(&kept, true, layers).unwrap_or(kept); + composite_overprints(&mut d, set); + Cow::Owned(d) } /// Overprinting fills and strokes in composite output: discarded (they knock out) or simulated diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e9f587d0e4..766f58d944 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1575,7 +1575,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Outline Object on an image's own fill or stroke follows the outline of the image's opaque area, and the image's frame when the image has no transparency. Path effects apply to a fill or stroke added to a bitmap object ([archived reference manual, page 561](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf#page=561)), and with Outline Object such a stroke follows the boundary of the art when the image has transparent areas and the image's frame when it has none ([CreativePro article on strokes for placed images](https://web.archive.org/web/20250815214529/https://creativepro.com/add-strokes-placed-images-illustrator/)). VectorCraft's own rules: the opaque area is the pixels at least 50% opaque, and its outline is traced as Image Trace's Black and White mode traces at threshold 128 and fitted with curves. Evidence: `tests::imageoutline` (vectorcraft-effects), `opaque::tests` (vectorcraft-trace), `tests::containers::an_images_stroke_with_outline_object_follows_its_opaque_area` and `tests::containers::an_images_outline_object_stroke_exports_selects_and_measures_along_its_opaque_area` (vectorcraft-engine), `tests::objectfx::an_images_outline_object_stroke_follows_new_pixels_under_the_same_key` (vectorcraft-render). A geometry effect on one of type's own fills or strokes reshapes the whole outline that the fill or stroke paints, on the canvas and in exports; PDF, SVG, EPS and the other vector exports write such type as paths. On type that holds inline graphics, those effects are drawn and exported only when the type has a geometry effect or perspective of its own. A geometry effect on a symbol instance's fill reshapes the filled shapes of the symbol's art and leaves their strokes as they are; one on the instance's stroke reshapes the stroked shapes and leaves their fills as they are. The shapes keep their colors. Only the instance's topmost fill and topmost stroke count, and only while they are visible with an opacity above 0. The canvas and Object › Expand Appearance use the same art, SVG and PDF export bake the same shapes, and Expand Appearance turns the instance into an editable group in one undo step. A group's own fills and strokes in the art take the effects too. An instance inside the art passes the effects on to its own art, and a dynamic symbol's instance reshapes the fills and strokes it paints on the art. How the effects map onto the symbol's art is VectorCraft's own rule. The help page describes effects on one fill or stroke, and the help page describes Expand Appearance (engine `expand::a_symbol_instance_fill_effect_reshapes_its_filled_art_and_expands`, `a_symbol_instance_stroke_effect_reshapes_its_stroked_art_and_expands`, `a_stroke_effect_reshapes_a_stroke_a_dynamic_instance_adds`, `a_fill_effect_reshapes_the_fill_of_a_group_in_the_art`, `only_the_topmost_visible_instance_fill_reshapes_the_art`, `a_nested_symbol_instance_takes_the_outer_fill_effect`, `export_bakes_a_symbol_instance_fill_effect`, `a_fill_effect_on_an_instance_of_a_symbol_that_holds_itself_ends`). Existing documents with a geometry effect on a symbol instance's fill or stroke draw differently: before, the effect drew nothing. - Live effects inside a symbol's art (geometry effects, color adjustments and the rest that export bakes) are written where each instance places the art in PDF, print, EPS and DXF, as the canvas draws them: the art is placed first and its effects are applied after, so an instance that scales its art does not scale the distance a Transform effect moves it. Symbols nested in the art, a dynamic symbol instance's own paint changes and the stain an instance's fill gives its art are kept, and the art is stained before its effects apply. A symbol whose art holds itself stops at the nesting limit, and the symbols' own art keeps its effects. An instance stays an instance in these exports when the objects its art makes once baked (the copies a Transform effect makes included) would take the export's placed symbol art past 16,000,000 objects, or when its baked art would draw deeper than the 128 levels a document may draw; these are VectorCraft's own limits. Before, these exports wrote the art without its effects (effects `bake::placing_symbols_bakes_their_art_where_each_instance_places_it`, `placing_symbols_reaches_the_art_of_nested_instances`, `placing_a_symbol_that_holds_itself_stops`, `placing_symbols_keeps_dynamic_instances_changes`, `placing_symbols_keeps_the_instances_stain`, `placing_symbols_counts_the_baked_art`, `placing_symbols_counts_nested_art_once`, `placing_symbols_keeps_the_baked_art_within_the_drawn_depth`; doc `depth::tests::drawn_heights_measure_objects_outside_the_layers`; PDF, print, EPS and DXF `symbol_art_effects_are_written_where_they_draw`, `symbol_art_effects_print_where_they_draw`; PDF `a_stained_instance_keeps_its_stain_when_its_art_has_effects`). + Live effects inside a symbol's art (geometry effects, color adjustments and the rest that export bakes) are written where each instance places the art in PDF, print, EPS and DXF, as the canvas draws them: the art is placed first and its effects are applied after, so an instance that scales its art does not scale the distance a Transform effect moves it. Symbols nested in the art, a dynamic symbol instance's own paint changes and the stain an instance's fill gives its art are kept, and the art is stained before its effects apply. A symbol whose art holds itself stops at the nesting limit, and the symbols' own art keeps its effects. An instance stays an instance in these exports when the objects its art makes once baked (the copies a Transform effect makes included) would take the export's placed symbol art past 16,000,000 objects, or when its baked art would draw deeper than the 128 levels a document may draw; these are VectorCraft's own limits. Instances the export leaves out (hidden ones, those in hidden groups, on template layers, and on hidden layers or layers that do not print when the export leaves those out) are not placed and do not count toward that limit. Before, these exports wrote the art without its effects (effects `bake::placing_symbols_bakes_their_art_where_each_instance_places_it`, `placing_symbols_reaches_the_art_of_nested_instances`, `placing_a_symbol_that_holds_itself_stops`, `placing_symbols_keeps_dynamic_instances_changes`, `placing_symbols_keeps_the_instances_stain`, `placing_symbols_counts_the_baked_art`, `placing_symbols_counts_nested_art_once`, `placing_symbols_keeps_the_baked_art_within_the_drawn_depth`, `placing_symbols_skips_instances_the_export_leaves_out`; doc `depth::tests::drawn_heights_measure_objects_outside_the_layers`; PDF, print, EPS and DXF `symbol_art_effects_are_written_where_they_draw`, `symbol_art_effects_print_where_they_draw`; PDF `a_stained_instance_keeps_its_stain_when_its_art_has_effects`). A raster effect (Drop Shadow, Outer Glow, Gaussian Blur and the other effects that paint pixels) on a symbol instance's topmost fill draws on each filled shape of the symbol's art, and one on its topmost stroke draws on each stroked shape, after any geometry effects listed before it, under the same rules as geometry effects: only a visible fill or stroke with an opacity above 0 counts, a group's own fills and strokes in the art take the effect, and an instance inside the art passes it on. Each shape gets its own shadow or glow, which is VectorCraft's own rule. The canvas, Use Preview Bounds and the drawn bounds, SVG and PDF export draw it, and Object › Expand Appearance is enabled for such an instance and makes the effect an image at the document's raster effects resolution, in one undo step (engine `expand::a_symbol_instance_fill_shadow_shadows_its_filled_art`, `a_symbol_instance_stroke_shadow_shadows_only_its_stroked_art`, `a_symbol_instance_fill_shadow_follows_the_fill_offset_before_it`, `a_nested_symbol_instance_takes_the_outer_fill_shadow`, `export_draws_a_symbol_instance_fill_shadow`). Existing documents with a raster effect on a symbol instance's fill or stroke draw differently: before, the effect drew nothing. The Appearance panel's inline effect editor labels each parameter as the effect dialogs' generic fields do, without the colon ("Max. Radius", "Channel 1", "Create New Fields by"). Roughen's and Zig Zag's Points, Offset Path's Joins, Inner Glow's Source and the menus of Radial Blur, Smart Blur, Sprayed Strokes, Glass, Mezzotint, Halftone Pattern, Grain, Texturizer and De-Interlace are dropdowns with the dialog's labels that send the dialog's values, and a plug-in's choice is a dropdown of the plug-in's values. Revolve's edge, Extrude & Bevel's bevel, every Warp style's orientation, Rasterize's Color Model, Background and Anti-aliasing and the channel of Levels and Curves are dropdowns too, with the labels their own dialogs show. `effect.apply` and `effect.setParams` refuse a Levels or Curves `channel` other than `rgb`, `red`, `green` or `blue` (or `r`, `g` and `b`, in any letter case).