From adea2d307d893c0e21b23dbafce8637658e75687 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 15 Oct 2026 14:58:17 -0700 Subject: [PATCH] Print: Print and Print Preview warn about type that doesn't fit its frame Area type and type on a path whose text doesn't fit print without the text past the end of the frame. text.create, text.createInPath, text.setText and text.setStyle report it in their replies, but file.print and print.preview gave no warning, and the Print dialog's Summary showed none. Both commands now add a warning that counts the area type and type on a path that prints and doesn't fit, and gives their ids (the first 20, then how many more). It counts the objects that the fallback font warning counts: visible objects on the layers that Print Layers picks, on the paper of the artboards or tiles that print. The walk that finds the fonts of the printed type also collects these ids. Type in the art of symbols, patterns, brushes, opacity masks and placed documents is not named. Docs: docs/mcp.md (Print, Fitting area type), docs/target-app-parity.md (Type). --- crates/engine/src/cmd/fonts.rs | 63 +++++++++++------ crates/engine/src/cmd/print.rs | 22 +++--- crates/engine/src/cmd/typecmd.rs | 17 +++++ crates/engine/src/tests_print.rs | 22 ++++++ crates/engine/src/tests_printpreview.rs | 91 +++++++++++++++++++++++++ crates/engine/src/tests_printps.rs | 14 ++++ docs/mcp.md | 7 +- docs/target-app-parity.md | 2 +- 8 files changed, 204 insertions(+), 34 deletions(-) diff --git a/crates/engine/src/cmd/fonts.rs b/crates/engine/src/cmd/fonts.rs index ea348294f..a0e16695b 100644 --- a/crates/engine/src/cmd/fonts.rs +++ b/crates/engine/src/cmd/fonts.rs @@ -114,9 +114,12 @@ pub(super) fn used_fonts(d: &Document) -> BTreeSet { fonts } -/// The fonts (family, style, version) of the type of `d` that prints on pages showing `areas` -/// (document space): visible type on `layers` (the layers that print) but not on a template layer -/// (of area type and type on a path that don't fit, the runs that their layout draws), +/// The type of `d` that prints on pages showing `areas` (document space): the fonts (family, style, +/// version) it is set in, and the ids of the area type and type on a path among the objects of the +/// document whose text doesn't fit (not those in the art of symbols, opacity masks, patterns, +/// brushes or placed documents). +/// The type that prints is visible type on `layers` (the layers that print) but not on a template +/// layer (of area type and type on a path that don't fit, the runs that their layout draws), /// where what it paints ([`vectorcraft_render::painted_bounds`], its effects applied) meets an area /// inside the clipping paths around it, the type in the opacity masks and patterns of the objects /// that print and in the pieces their brushes draw, and the type of the symbols that their @@ -125,27 +128,32 @@ pub(super) fn used_fonts(d: &Document) -> BTreeSet { /// fills or strokes of its own, are taken with it when what it paints meets an area, and so is the /// art of a placed document (`d` as [`Document::with_placed_art`] gives it, for that art's /// resources). -pub(super) fn printed_fonts(d: &Document, layers: &[Arc], areas: &[Rect]) -> BTreeSet { - let mut walk = Printed { d, fonts: BTreeSet::new(), symbols: BTreeSet::new(), patterns: BTreeSet::new() }; +pub(super) fn printed_type(d: &Document, layers: &[Arc], areas: &[Rect]) -> (BTreeSet, BTreeSet) { + let mut walk = Printed { d, own: true, fonts: BTreeSet::new(), overflow: BTreeSet::new(), symbols: BTreeSet::new(), patterns: BTreeSet::new() }; for l in layers { walk.node(l, Some(areas)); } - walk.fonts + (walk.fonts, walk.overflow) } -/// The walk of [`printed_fonts`]. +/// The walk of [`printed_type`]. struct Printed<'a> { d: &'a Document, + /// Whether the walk is among the objects of the document, not in art drawn for one. + own: bool, fonts: BTreeSet, + /// The objects of the document whose text doesn't fit. + overflow: BTreeSet, /// The symbols and patterns whose art was walked. symbols: BTreeSet, patterns: BTreeSet, } impl Printed<'_> { - /// Adds the fonts of `n` and its descendants that print: those whose bounds meet one of `areas`, - /// or with `None` (in the art of a symbol, opacity mask, pattern or placed document, in brush - /// pieces, or in a live object) all of them but those outside the clipping path of a clip group. + /// Adds the fonts of `n` and its descendants that print, and the ids of those that don't fit + /// while [`Printed::own`] is set: those whose bounds meet one of `areas`, or with `None` (in the + /// art of a symbol, opacity mask, pattern or placed document, in brush pieces, or in a live + /// object) all of them but those outside the clipping path of a clip group. fn node(&mut self, n: &Node, areas: Option<&[Rect]>) { if !n.visible || matches!(n.kind, NodeKind::Layer { template: true, .. }) { return; @@ -162,9 +170,9 @@ impl Printed<'_> { } // The art that its opacity mask, pattern paints and brushed strokes draw. let mask = n.extras().mask.as_deref().filter(|m| !m.disabled); - let runs = match &n.kind { + let (runs, overflows) = match &n.kind { NodeKind::Text(t) => drawn_runs(t), - _ => vec![], + _ => (vec![], false), }; let items: Vec<&AppearanceItem> = n.appearance.items.iter().filter(|i| i.visible() && !matches!(i, AppearanceItem::Stroke(s) if s.width <= 0.0)).collect(); @@ -180,7 +188,7 @@ impl Printed<'_> { let brushed = vectorcraft_brush::has_brush(n); if (mask.is_some() || !patterns.is_empty() || brushed) && areas.is_none_or(|a| !open || self.meets(n, a)) { if let Some(m) = mask { - self.node(&m.art, None); + self.art(&m.art); } let d = self.d; for name in patterns { @@ -188,19 +196,22 @@ impl Printed<'_> { && let Some(p) = d.pattern(name) { for art in &p.art { - self.node(art, None); + self.art(art); } } } // The pieces its brushes draw along the path (art and pattern brushes leave type out). if brushed { for piece in vectorcraft_brush::node_pieces(d, n).unwrap_or_default() { - self.node(&piece, None); + self.art(&piece); } } } match &n.kind { NodeKind::Text(_) => { + if overflows && self.own { + self.overflow.insert(n.id); + } for r in runs { self.fonts.insert((r.style.font_family.clone(), r.style.font_style.clone(), r.style.font_version.clone())); // Inline graphics draw their symbols' art. @@ -212,7 +223,7 @@ impl Printed<'_> { NodeKind::SymbolInstance { symbol, .. } => self.symbol(symbol), NodeKind::PlacedDocument(p) => { for art in p.art(n) { - self.node(&art, None); + self.art(&art); } } NodeKind::Layer { children, clip, .. } | NodeKind::Group { children, clip } if !moved => { @@ -240,10 +251,19 @@ impl Printed<'_> { if self.symbols.insert(name.to_string()) && let Some(s) = d.symbols.iter().find(|s| s.name == name) { - self.node(&s.art, None); + self.art(&s.art); } } + /// Adds the fonts of `art`, drawn for an object of the document but not one of its objects (the + /// art of a symbol, opacity mask, pattern or placed document, or a brush piece), with + /// [`Printed::node`]'s `None`. + fn art(&mut self, art: &Node) { + let own = std::mem::replace(&mut self.own, false); + self.node(art, None); + self.own = own; + } + /// Whether what `n` paints, its effects applied, meets one of `areas`. fn meets(&self, n: &Node, areas: &[Rect]) -> bool { vectorcraft_render::painted_bounds(self.d, n).is_some_and(|b| areas.iter().any(|r| b.intersect(*r).area() > 0.0 || r.contains_rect(b))) @@ -256,14 +276,15 @@ impl Printed<'_> { } } -/// The runs of `t` that print: all of them, or of area type and type on a path that don't fit, the -/// runs that their layout draws glyphs or inline graphics of. -fn drawn_runs(t: &vectorcraft_doc::TextObject) -> Vec<&TextRun> { +/// The runs of `t` that print, and whether its text doesn't fit: all of them, or of area type and +/// type on a path that don't fit, the runs that their layout draws glyphs or inline graphics of. +fn drawn_runs(t: &vectorcraft_doc::TextObject) -> (Vec<&TextRun>, bool) { let drawn: Option> = (!matches!(t.kind, TextKind::Point)) .then(|| vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t)) .filter(|lay| lay.overflow) .map(|lay| lay.glyphs.iter().map(|g| g.run).chain(lay.inlines.iter().map(|i| i.run)).collect()); - t.runs.iter().enumerate().filter(|(i, _)| drawn.as_ref().is_none_or(|d| d.contains(i))).map(|(_, r)| r).collect() + let runs = t.runs.iter().enumerate().filter(|(i, _)| drawn.as_ref().is_none_or(|d| d.contains(i))).map(|(_, r)| r).collect(); + (runs, drawn.is_some()) } /// What an export that draws type (as outlines, embedded fonts or pixels) says when `d` uses font diff --git a/crates/engine/src/cmd/print.rs b/crates/engine/src/cmd/print.rs index 258acc53a..4821edebd 100644 --- a/crates/engine/src/cmd/print.rs +++ b/crates/engine/src/cmd/print.rs @@ -31,7 +31,7 @@ pub fn specs() -> Vec { "Print Preview", [], None, - "{settings?: {…settings of print.setup} (over the document's)} → {pages (copies included), sheets: [{artboard (0-based; null with artboards ignored), tile? (1-based), ink? (separations), width, height (pt, the page), orientation, scale: [width %, height %], transform: [a, b, c, d, e, f] (document space → the page, pt, y down), area: [x0, y0, x1, y1] (the document region the page prints), trim: [x0, y0, x1, y1] (the artboard on the page)}] (one copy, in order), tiles: [{artboard, columns, rows, printed: [1-based tiles], tiles: [[x0, y0, x1, y1]…] (document space)}], inks: [{name, spot, print, frequency, angle}] (separations), warnings (art larger than the imageable area, type that prints in the fallback font, options not applied yet…), settings} without printing", + "{settings?: {…settings of print.setup} (over the document's)} → {pages (copies included), sheets: [{artboard (0-based; null with artboards ignored), tile? (1-based), ink? (separations), width, height (pt, the page), orientation, scale: [width %, height %], transform: [a, b, c, d, e, f] (document space → the page, pt, y down), area: [x0, y0, x1, y1] (the document region the page prints), trim: [x0, y0, x1, y1] (the artboard on the page)}] (one copy, in order), tiles: [{artboard, columns, rows, printed: [1-based tiles], tiles: [[x0, y0, x1, y1]…] (document space)}], inks: [{name, spot, print, frequency, angle}] (separations), warnings (art larger than the imageable area, type that prints in the fallback font, type that doesn't fit its frame, options not applied yet…), settings} without printing", has_doc, preview ), @@ -92,7 +92,7 @@ fn preview(s: &mut Session, p: &Value) -> Result { let set = settings(C, doc, p)?; super::printadvanced::flattener(C, &set, &s.prefs.flattener_presets)?; let mut pv = vectorcraft_pdf::preview(doc, &set).map_err(|e| pdf_error(C, e))?; - pv.warnings.extend(fonts_warning(doc, set.print_layers, &areas(&pv))); + pv.warnings.extend(type_warnings(doc, set.print_layers, &areas(&pv))); let mut v = serde_json::to_value(pv).map_err(|e| EngineError::Other(e.to_string()))?; v["settings"] = to_json(&set)?; Ok(v) @@ -118,14 +118,16 @@ fn on_page(area: Rect, to_page: Affine, (w, h): (f64, f64)) -> Option { (r.area() > 0.0).then_some(r) } -/// The warning about type in `doc` that prints in the fallback font: on the layers `which` prints, -/// on pages that print `areas`. -fn fonts_warning(doc: &Document, which: PrintLayers, areas: &[Rect]) -> Option { +/// The warnings about the type in `doc` that prints (on the layers `which` prints, on pages that +/// print `areas`): type in the fallback font, and type that doesn't fit its frame. +fn type_warnings(doc: &Document, which: PrintLayers, areas: &[Rect]) -> Vec { // Placed documents print their files' art, which names the resources this adds. let doc = doc.with_placed_art(); let mut layers = doc.layers.clone(); vectorcraft_pdf::keep_layers(&mut layers, which); - super::fonts::substitution_warning_of(super::fonts::printed_fonts(&doc, &layers, areas)) + let (fonts, overflow) = super::fonts::printed_type(&doc, &layers, areas); + let overflow: Vec<_> = overflow.into_iter().collect(); + super::fonts::substitution_warning_of(fonts).into_iter().chain(super::typecmd::overflow_warning(&overflow)).collect() } const PRINT: &str = "file.print"; @@ -162,13 +164,13 @@ fn print(s: &mut Session, p: &Value) -> Result { // Print as Bitmap and the flattener preset (Advanced) render and flatten it first. let prepared = super::printadvanced::prepare(PRINT, doc, &set, &s.prefs.flattener_presets, s.prefs.black_output == "rich")?; let path = str_param(p, "path"); - // Fonts are read from the type before raster effects and Print as Bitmap turn it into images. + // The type is read before raster effects and Print as Bitmap turn it into images. let which = set.print_layers; if !postscript { let printed = prepared.as_ref().unwrap_or(doc); - let fonts = fonts_warning(&full, which, &areas(&vectorcraft_pdf::preview(printed, &set).map_err(|e| pdf_error(PRINT, e))?)); + let typed = type_warnings(&full, which, &areas(&vectorcraft_pdf::preview(printed, &set).map_err(|e| pdf_error(PRINT, e))?)); let mut r = vectorcraft_pdf::print(printed, &PrintOptions { settings: set, ..Default::default() }).map_err(|e| pdf_error(PRINT, e))?; - r.warnings.extend(fonts); + r.warnings.extend(typed); return write_or_return(path, &r.bytes, json!({ "pages": r.pages, "format": "pdf", "warnings": r.warnings })); } let level = match p.get("level").filter(|v| !v.is_null()) { @@ -212,6 +214,6 @@ fn print(s: &mut Session, p: &Value) -> Result { } } let areas: Vec = plan.sheets.iter().filter_map(|sh| on_page(sh.area, sh.view * sh.place, sh.size)).collect(); - warnings.extend(fonts_warning(&full, which, &areas)); + warnings.extend(type_warnings(&full, which, &areas)); write_or_return(path, &out.bytes, json!({ "pages": plan.order.len(), "format": "postscript", "warnings": warnings })) } diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index e330af600..d1ffde26a 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -269,6 +269,23 @@ pub(crate) fn with_overflow(s: &Session, ids: &[NodeId], mut result: Value) -> V result } +/// The warning that Print gives for `ids`, area type and type on a path whose text doesn't fit +/// (as [`with_overflow`] finds it): the text past the end of their frames doesn't print. It names +/// the first 20 ids and counts the rest; `None` when `ids` is empty. +pub(crate) fn overflow_warning(ids: &[NodeId]) -> Option { + const NAMED: usize = 20; + let (named, rest) = ids.split_at(ids.len().min(NAMED)); + let mut list = named.iter().map(|i| i.0.to_string()).collect::>().join(", "); + if !rest.is_empty() { + list = format!("{list} and {} more", rest.len()); + } + match ids { + [] => None, + [_] => Some(format!("1 text object doesn't fit its frame: the text past the end of the frame doesn't print (id {list})")), + _ => Some(format!("{} text objects don't fit their frames: the text past the end of each frame doesn't print (ids {list})", ids.len())), + } +} + /// Text objects among `ids` and their descendants. fn text_ids(d: &Document, ids: &[NodeId]) -> Vec { let mut out = vec![]; diff --git a/crates/engine/src/tests_print.rs b/crates/engine/src/tests_print.rs index 553ec12f9..5094ed8ea 100644 --- a/crates/engine/src/tests_print.rs +++ b/crates/engine/src/tests_print.rs @@ -130,3 +130,25 @@ fn type_in_a_missing_family_warns_as_pdf_export_does() { assert_eq!(fallback(&v), export, "{settings}"); } } + +#[test] +fn type_that_does_not_fit_its_frame_is_named() { + let (mut s, _) = session(); + let p = json!({"x": 10, "y": 100, "text": "Ab\nCd\nEf\nGh", "size": 12, "area": {"width": 100, "height": 20}}); + let id = s.execute("text.create", &p).unwrap()["id"].clone(); + let over = |v: &Value| -> Vec { + v["warnings"].as_array().unwrap().iter().filter_map(Value::as_str).filter(|w| w.contains("fit its frame")).map(str::to_string).collect() + }; + let (_, v) = print(&mut s, json!({})); + assert_eq!(over(&v), [format!("1 text object doesn't fit its frame: the text past the end of the frame doesn't print (id {id})")]); + assert_eq!(over(&v), over(&s.execute("print.preview", &json!({})).unwrap())); + // Type printed as an image is named too. + s.execute("effect.apply", &json!({"effect": "stylize.dropShadow", "ids": [id]})).unwrap(); + for settings in [json!({}), json!({"advanced": {"printAsBitmap": true}})] { + let (_, v) = print(&mut s, json!({ "settings": settings })); + assert_eq!(over(&v).len(), 1, "{settings}"); + } + s.execute("text.areaOptions", &json!({"ids": [id], "height": 150})).unwrap(); + let (_, v) = print(&mut s, json!({})); + assert_eq!(over(&v), Vec::::new()); +} diff --git a/crates/engine/src/tests_printpreview.rs b/crates/engine/src/tests_printpreview.rs index ee37317c1..ebe9be9e5 100644 --- a/crates/engine/src/tests_printpreview.rs +++ b/crates/engine/src/tests_printpreview.rs @@ -140,6 +140,97 @@ fn type_that_does_not_fit_its_frame_counts_only_where_it_shows() { assert_eq!(preview_fallback(&mut s, json!({})), Vec::::new()); } +/// Area type 100 × 20 pt at (`x`, `y`) holding four lines, of which the first shows → its id. +fn overset(s: &mut Session, x: f64, y: f64) -> u64 { + let v = s.execute("text.create", &json!({"x": x, "y": y, "text": "Ab\nCd\nEf\nGh", "size": 12, "area": {"width": 100, "height": 20}})).unwrap(); + assert_eq!(v["overflow"], json!([v["id"]]), "{v}"); + v["id"].as_u64().unwrap() +} + +/// `print.preview {settings}`'s warnings about type that doesn't fit its frame. +fn preview_overflow(s: &mut Session, settings: Value) -> Vec { + let v = s.execute("print.preview", &json!({ "settings": settings })).unwrap(); + let over = |w: &&str| w.contains("fit its frame") || w.contains("fit their frames"); + v["warnings"].as_array().unwrap().iter().filter_map(Value::as_str).filter(over).map(str::to_string).collect() +} + +#[test] +fn type_that_does_not_fit_its_frame_is_named() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap(); + let id = overset(&mut s, 10.0, 10.0); + let one = format!("1 text object doesn't fit its frame: the text past the end of the frame doesn't print (id {id})"); + assert_eq!(preview_overflow(&mut s, json!({})), [one]); + // A frame that holds its text, or a hidden object, leaves nothing out. + s.execute("text.areaOptions", &json!({"ids": [id], "height": 150})).unwrap(); + assert_eq!(preview_overflow(&mut s, json!({})), Vec::::new()); + s.execute("text.areaOptions", &json!({"ids": [id], "height": 20})).unwrap(); + s.execute("object.setProps", &json!({"ids": [id], "visible": false})).unwrap(); + assert_eq!(preview_overflow(&mut s, json!({})), Vec::::new()); + s.execute("object.setProps", &json!({"ids": [id], "visible": true})).unwrap(); + // Type on a path that runs past the path's end. + let path = s.execute("path.create", &json!({"d": "M10 150 L60 150"})).unwrap()["id"].clone(); + let on = s.execute("text.createInPath", &json!({"path": path, "mode": "onPath", "text": "Longer than its path"})).unwrap(); + assert_eq!(on["overflow"], json!([on["id"]]), "{on}"); + let both = format!("2 text objects don't fit their frames: the text past the end of each frame doesn't print (ids {id}, {})", on["id"]); + assert_eq!(preview_overflow(&mut s, json!({})), [both]); + // Past 20, the first 20 ids and how many more. + let mut ids = vec![id, on["id"].as_u64().unwrap()]; + ids.extend((0..20).map(|i| overset(&mut s, 150.0, f64::from(i) * 5.0))); + let named: Vec = ids[..20].iter().map(u64::to_string).collect(); + let many = + format!("22 text objects don't fit their frames: the text past the end of each frame doesn't print (ids {} and 2 more)", named.join(", ")); + assert_eq!(preview_overflow(&mut s, json!({})), [many]); +} + +#[test] +fn type_that_does_not_fit_counts_only_layers_and_artboards_that_print() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 300, "height": 200, "artboards": 2})).unwrap(); + let r = s.doc().unwrap().doc.artboards[1].rect; + overset(&mut s, r.x0 + 10.0, r.y0 + 10.0); + assert_eq!(preview_overflow(&mut s, json!({"artboards": "range", "range": "1"})), Vec::::new()); + assert_eq!(preview_overflow(&mut s, json!({"artboards": "range", "range": "2"})).len(), 1); + // On a hidden layer, the type prints only with Print Layers: All. + let layer = s.execute("layer.new", &json!({})).unwrap()["id"].as_u64().unwrap(); + overset(&mut s, 10.0, 10.0); + s.execute("layer.setProps", &json!({"id": layer, "visible": false})).unwrap(); + for (layers, warns) in [("visiblePrintable", false), ("visible", false), ("all", true)] { + let warned = preview_overflow(&mut s, json!({"artboards": "range", "range": "1", "printLayers": layers})); + assert_eq!(!warned.is_empty(), warns, "{layers}"); + } + // A layer that doesn't print. + s.execute("layer.setProps", &json!({"id": layer, "visible": true, "printable": false})).unwrap(); + assert_eq!(preview_overflow(&mut s, json!({"artboards": "range", "range": "1"})), Vec::::new()); + assert_eq!(preview_overflow(&mut s, json!({"artboards": "range", "range": "1", "printLayers": "visible"})).len(), 1); +} + +#[test] +fn type_that_does_not_fit_is_named_only_among_the_objects_of_the_document() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap(); + let id = overset(&mut s, 10.0, 10.0); + assert_eq!(preview_overflow(&mut s, json!({})).len(), 1); + s.execute("symbol.new", &json!({"name": "Label", "ids": [id]})).unwrap(); + assert_eq!(preview_overflow(&mut s, json!({})), Vec::::new(), "the art of a symbol"); +} + +#[test] +fn a_threaded_story_is_named_only_when_its_last_frame_does_not_hold_it() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 600, "height": 300})).unwrap(); + let story = "The quick brown fox jumps over the lazy dog. ".repeat(8); + let a = s.execute("text.create", &json!({"x": 10, "y": 10, "text": story, "area": {"width": 150, "height": 60}})).unwrap(); + assert_eq!(a["overflow"], json!([a["id"]]), "{a}"); + let b = s.execute("shape.rectangle", &json!({"x": 200, "y": 10, "width": 150, "height": 200})).unwrap()["id"].clone(); + s.execute("select.set", &json!({"ids": [a["id"], b]})).unwrap(); + s.execute("text.thread.create", &json!({})).unwrap(); + assert_eq!(preview_overflow(&mut s, json!({})), Vec::::new(), "the second frame holds the rest"); + s.execute("text.editRange", &json!({"id": a["id"], "start": 0, "end": 0, "insert": story.repeat(4)})).unwrap(); + let warned = preview_overflow(&mut s, json!({})); + assert!(warned.len() == 1 && warned[0].ends_with(&format!("(id {b})")), "{warned:?}"); +} + #[test] fn type_of_an_artboard_larger_than_the_paper_counts_only_on_the_paper() { let mut s = Session::new(); diff --git a/crates/engine/src/tests_printps.rs b/crates/engine/src/tests_printps.rs index b80ac0611..57cfbc901 100644 --- a/crates/engine/src/tests_printps.rs +++ b/crates/engine/src/tests_printps.rs @@ -111,3 +111,17 @@ fn type_in_a_missing_family_is_named_when_its_artboard_prints() { let (_, v) = print_ps(&mut s, json!({ "settings": paper })); assert_eq!(fallback(&v), 0, "{v}"); } + +#[test] +fn type_that_does_not_fit_its_frame_is_named_when_its_artboard_prints() { + let mut s = session(2); + let r = s.doc().unwrap().doc.artboards[1].rect; + let p = json!({"x": r.x0 + 10.0, "y": r.y0 + 10.0, "text": "Ab\nCd\nEf\nGh", "size": 12, "area": {"width": 100, "height": 20}}); + let id = s.execute("text.create", &p).unwrap()["id"].clone(); + let over = |v: &Value| v["warnings"].as_array().unwrap().iter().filter(|w| w.as_str().unwrap().contains("fit its frame")).count(); + let (_, v) = print_ps(&mut s, json!({})); + assert_eq!(over(&v), 1, "{v}"); + assert!(v["warnings"].to_string().contains(&format!("(id {id})")), "{v}"); + let (_, v) = print_ps(&mut s, json!({"settings": {"artboards": "range", "range": "1"}})); + assert_eq!(over(&v), 0, "{v}"); +} diff --git a/docs/mcp.md b/docs/mcp.md index cb7c7c27f..38d0a0e8a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2682,7 +2682,9 @@ as they do for art larger than the imageable area. Type in a font family that is font, and the warnings of `file.print` and `print.preview` name those families. They count only type that prints: visible type that fits its frame or path, on the layers `printLayers` picks, on the paper of the artboards or tiles that print, and the type in the symbols, patterns, scatter brushes, opacity masks and placed documents of the art that -prints. +prints. The warnings also give the ids of the area type and type on a path that print and whose text doesn't fit +(objects of the document only, not type in the art of symbols, patterns, brushes, opacity masks or placed documents; +see [Fitting area type](#fitting-area-type-auto-size-shrink-text-to-fit-overflow)). ```json {"name":"run_command","arguments":{"command":"print.setup","params":{"settings":{"media":"a4","scaling":"fit","marks":{"trim":true}}}}} @@ -3124,7 +3126,8 @@ Text that doesn't fit is drawn up to the last line that does, so a one-line head draws nothing. The commands that make type or change what it holds say so in their own reply: `text.create`, `text.createInPath`, `text.setText` and `text.setStyle` add `overflow: [id…]` (the area type and type on a path among the objects they made or changed whose text doesn't fit), and the `add_text` tool adds `overflow: true`. The key is -left out when everything fits. +left out when everything fits. The warnings of `file.print` and `print.preview` give the ids of the objects that print +and whose text doesn't fit: the first 20 ids, then how many more. ```json {"name":"run_command","arguments":{"command":"text.areaOptions","params":{"fit":"shrinkText","fitMinPercent":60}}} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 94596ad4a..7f94b53dd 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -548,7 +548,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Type:** Character size, leading, baseline shift and horizontal-scale controls account for object/group transforms (#1034): exact document-point sizes, mixed-value display and keyboard increments, range edits, resettable horizontal stretch, undo/redo and native save/reopen (`tests_typescale`, Character panel tests). Point size is the transformed em-height axis, independent of glyph ink bounds; affine rotation, reflection and shear are retained. Group scaling still scales text; preserving font sizes is a separate enhancement. Behaviour reference: [Transform text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/transform-text.html) (public documentation). Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. A double-click on area type's bottom middle handle turns on Auto Size (#1940). Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. Variation axis sliders are not done. Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boxes, the rest in the background). Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724). The commands that make or restyle type (and the MCP `add_text` tool) say in their result when area type does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471). A tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458). Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615). Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607). Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656). Select › Same matches type on its characters' fill and stroke (#1746, #1737). `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748). - **Vertical punctuation:** fullwidth `!` (U+FF01) and `?` (U+FF1F) use their nominal glyph upright when no vertical alternate is present, following the Tu fallback in [Unicode UAX #50 §3.1](https://www.unicode.org/reports/tr50/#vo). Code-built test fonts cover point and area type with and without `vert` alternates, retain the rotated fallback for a bracket and long vowel mark, and leave horizontal type unchanged. Tests: `crates/text/src/tests_vertical.rs`. - Free Distort's dialog (Effect › Distort & Transform › Free Distort…) shows a box in the proportions of the first selected object's bounds with a handle on each corner. Dragging a handle moves that corner. A handle focused by a click or Tab moves 1% of the box with each arrow key, and 10% with Shift. Reset puts the four corners back on the box. The preview and OK run `effect.apply`, or `effect.setParams` when the dialog edits an applied effect, and the Appearance panel's inline editor opens the dialog with Edit…. Behavior reference: Illustrator CC Help (2014, [PDF](https://help.adobe.com/archive/en/illustrator/cc/2014/illustrator_reference.pdf)), page 285 (PDF page 291). -- **Type:** Character size, leading, baseline shift and horizontal-scale controls account for object/group transforms (#1034): exact document-point sizes, mixed-value display and keyboard increments, range edits, resettable horizontal stretch, undo/redo and native save/reopen (`tests_typescale`, Character panel tests). Point size is the transformed em-height axis, independent of glyph ink bounds; affine rotation, reflection and shear are retained. Group scaling still scales text; preserving font sizes is a separate enhancement. Behaviour reference: [Transform text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/transform-text.html) (public documentation). Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. Variation axis sliders are not done. Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boxes, the rest in the background). Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724). The commands that make or restyle type (and the MCP `add_text` tool) say in their result when area type does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471). A tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458). Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615). Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607). Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656). Select › Same matches type on its characters' fill and stroke (#1746, #1737). `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748). +- **Type:** Character size, leading, baseline shift and horizontal-scale controls account for object/group transforms (#1034): exact document-point sizes, mixed-value display and keyboard increments, range edits, resettable horizontal stretch, undo/redo and native save/reopen (`tests_typescale`, Character panel tests). Point size is the transformed em-height axis, independent of glyph ink bounds; affine rotation, reflection and shear are retained. Group scaling still scales text; preserving font sizes is a separate enhancement. Behaviour reference: [Transform text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/transform-text.html) (public documentation). Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. Variation axis sliders are not done. Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boxes, the rest in the background). Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724). The commands that make or restyle type (and the MCP `add_text` tool) say in their result when area type does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471). Print and Print Preview warn about the area type and type on a path that prints without the text that doesn't fit, and give their ids. A tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458). Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615). Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607). Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656). Select › Same matches type on its characters' fill and stroke (#1746, #1737). `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748). - **Paragraph alignment:** the shared Paragraph/Properties alignment row leaves every button unselected when the paragraphs it acts on differ. Auto resolves per paragraph; choosing an alignment uses the existing command. Headless paragraph_mixed_alignment_* regressions exercise mixed → Center → Undo, caret/uniform states, Unicode range boundaries, Auto/direction, split runs and grouped text. These regressions and full integration CI pass. Original captures confirm mixed → Center → Undo in the existing alignment-button layout. The [public Paragraph panel guide](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/paragraph-panel-overview.html) lists the seven controls, but does not specify mixed-state rendering. Other paragraph fields remain outside this change; scores and hours unchanged. For Auto Size area type in columns, the bisection for the frame's height runs on the lines of the one-column layout, and its result is checked with at most two layouts of the columns. When the text wraps around objects or does not fit the one-column layout, or when the check fails, the columns are laid out at every step of the bisection, as before. Typing at the start of a story of 50,000 characters in 2 columns took 1,670 ms a keystroke and now takes 292 ms (release build, `auto_height_in_columns_typing_timing`). An edit copies a thread's story only when it changes one of the thread's frames: with a story of 50,000 characters threaded through 8 frames with a style change at every word, moving another object took 7.8 ms and now takes 0.013 ms, and a frame of a drag took 7.2 ms and now takes 0.012 ms (release build, `threaded_story_edit_timing`).