diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index fd7a5232..36d54de9 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -1089,7 +1089,7 @@ fn columns(s: &mut Session, p: &Value) -> Result { } fn autofit(s: &mut Session, p: &Value) -> Result { let m = match str_param(p, "mode").unwrap_or("none") { - "shrink" => AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }, + "shrink" => AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }, "resize" | "shape" => AutoFit::Shape, _ => AutoFit::None, }; diff --git a/crates/engine/src/cmd/smartart.rs b/crates/engine/src/cmd/smartart.rs index a483197c..c58d1830 100644 --- a/crates/engine/src/cmd/smartart.rs +++ b/crates/engine/src/cmd/smartart.rs @@ -541,7 +541,7 @@ fn shadow(blur: f64, dist: f64) -> Shadow { /// `title` false every line is a bullet (the bullet boxes of the bullet lists). fn text_shape(preset: &str, x: Xfrm, lines: &[&DiagramNode], title: bool) -> Shape { let mut body = TextBody::default(); - body.body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }); + body.body.autofit = Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }); let top = lines.first().map(|n| if title { n.level } else { n.level.saturating_sub(1) }).unwrap_or(0); for (k, n) in lines.iter().enumerate() { let mut para = Paragraph::new(&n.text); @@ -1913,7 +1913,7 @@ fn to_text(s: &mut Session, p: &Value) -> Result { body.paragraphs.push(Paragraph::new("")); } body.body.wrap = Some(true); - body.body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }); + body.body.autofit = Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }); let id = ShapeId(doc.alloc_id().ok_or_else(|| bad(CMD, NO_ID_SPACE))?); let tb = Shape { id, name: "Text".into(), xfrm: Some(area), text_box: true, text: Some(body), geom: Geom::preset("rect"), ..Default::default() }; diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index 9c6d88e1..a0408eff 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -119,9 +119,14 @@ pub(crate) fn edit_text(s: &mut Session, f: impl FnOnce(&mut TextBody, &mut T let mut t = t0.clone(); let body = body_mut(doc, sel, &t).ok_or_else(|| bad("text", "the text being edited is gone"))?; let r = f(body, &mut t); - // The saved shrink-on-overflow scale was for the old text: fit the new text afresh. + // The saved shrink-on-overflow scale was for the old text: fit the new text afresh. A + // body whose file carried no `fontScale` keeps none (100 %) here; `fit_shrink_scale` + // below fills in the computed scale when the new text overflows (#724). if let Some(AutoFit::Shrink { font_scale, line_reduction }) = &mut body.body.autofit { - (*font_scale, *line_reduction) = (1.0, 0.0); + if font_scale.is_some() { + *font_scale = Some(1.0); + } + *line_reduction = 0.0; } sel.text = Some(t); Ok(r) @@ -132,8 +137,11 @@ pub(crate) fn edit_text(s: &mut Session, f: impl FnOnce(&mut TextBody, &mut T Ok(r) } -/// Grow/shrink an auto-sizing text box (Resize shape to fit text) to its text. +/// Refit a shape after its text changed: remember the shrink-on-overflow scale the layout +/// computes (#724), then grow/shrink an auto-sizing text box (Resize shape to fit text) to its +/// text. pub fn fit_text_box(s: &mut Session, id: ShapeId) -> Result<()> { + fit_shrink_scale(s, id)?; let st = s.doc()?; let Some(sh) = st.shape(id).cloned() else { return Ok(()) }; let Some(body) = sh.text.clone() else { return Ok(()) }; @@ -171,6 +179,53 @@ pub fn fit_text_box(s: &mut Session, id: ShapeId) -> Result<()> { Ok(()) } +/// Store the shrink-on-overflow scale the layout computes for `id`, so a save keeps +/// `fontScale`/`lnSpcReduction` in step with what DeckCraft draws (#724). Runs after a text +/// change: a body with no meaningful saved scale — a file's bare `` (`None`) or +/// the reset an edit leaves (`1.0`, no reduction) — is fitted afresh; a scale the file saved is +/// left as it is, as PowerPoint draws it. +fn fit_shrink_scale(s: &mut Session, id: ShapeId) -> Result<()> { + let st = s.doc()?; + let Some(sh) = st.shape(id).cloned() else { return Ok(()) }; + let Some(body) = sh.text.clone() else { return Ok(()) }; + let Some(AutoFit::Shrink { font_scale, line_reduction }) = body.body.autofit else { return Ok(()) }; + if !(font_scale.is_none() || (font_scale == Some(1.0) && line_reduction == 0.0)) { + return Ok(()); + } + let doc = st.doc.clone(); + let sel = st.selection.clone(); + let Some(ctx) = ctx_for(&doc, &sel) else { return Ok(()) }; + let x = xfrm_of(&doc, &sel, &sh); + let tr = deckcraft_render::shape_geometry(&sh, x.w, x.h).text_rect; + // Lay out as if the scale had just been reset, so the fit is computed even for `None` (which + // is drawn at 100 % and only fitted again when its text changes). + let mut probe = body.clone(); + probe.body.autofit = Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }); + let opts = deckcraft_text::Opts { rect: tr, fields: &deckcraft_text::NoFields, prompt_color: None, no_shrink: false }; + let l = deckcraft_text::layout(&ctx, &sh, &probe, &opts); + if l.font_scale >= 1.0 && l.line_reduction <= 0.0 { + return Ok(()); + } + // Change the body in place without an extra undo step (it is part of the edit). + let st = s.doc_mut()?; + let mut d = (*st.doc).clone(); + let stored = if let Some(list) = crate::shapes_mut(&mut d, &st.selection) + && let Some(target) = deckcraft_model::find_shape_mut(list, id) + && let Some(t) = target.text.as_mut() + && let Some(AutoFit::Shrink { font_scale, line_reduction }) = &mut t.body.autofit + { + (*font_scale, *line_reduction) = (Some(l.font_scale), l.line_reduction); + true + } else { + false + }; + if stored { + st.doc = Arc::new(d); + st.revision += 1; + } + Ok(()) +} + pub(crate) fn ctx_for<'a>(doc: &'a Presentation, sel: &Selection) -> Option> { match sel.target { Target::Slides => doc.slides.get(sel.slide).and_then(|s| Ctx::for_slide(doc, s)), diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 19da6b09..1ce0725d 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -105,13 +105,45 @@ fn editing_text_refits_a_saved_shrink() { s.edit(|doc, sel| { let shapes = crate::shapes_mut(doc, sel).unwrap(); let sh = deckcraft_model::find_shape_mut(shapes, title).unwrap(); - sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: 0.5, line_reduction: 0.2 }); + sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: Some(0.5), line_reduction: 0.2 }); Ok(()) }) .unwrap(); s.execute("text.insert", &json!({"text": "!"})).unwrap(); let sh = s.doc().unwrap().shape(title).cloned().unwrap(); - assert_eq!(sh.text.unwrap().body.autofit, Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 })); + assert_eq!(sh.text.unwrap().body.autofit, Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 })); +} + +/// After an edit, the shrink-on-overflow scale the layout computes is stored, so the saved file +/// carries the scale DeckCraft draws instead of a bare normAutofit (#724). +#[test] +fn editing_shrink_text_stores_the_layout_scale() { + use deckcraft_model::text::AutoFit; + let mut s = session(); + let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [80.0, 80.0, 200.0, 40.0]})).unwrap(); + let id = deckcraft_model::ShapeId(r["id"].as_u64().unwrap() as u32); + s.execute("format.autofit", &json!({"mode": "shrink"})).unwrap(); + let text = (0..12).map(|i| format!("Line {i}")).collect::>().join("\n"); + s.execute("text.set", &json!({"id": id.0, "text": text})).unwrap(); + let sh = s.doc().unwrap().shape(id).cloned().unwrap(); + let Some(AutoFit::Shrink { font_scale, line_reduction }) = sh.text.unwrap().body.autofit else { panic!("shrink") }; + assert!(font_scale.is_some_and(|s| s < 1.0), "the computed fit is stored: {font_scale:?} / {line_reduction}"); + // The saved file must carry the scale DeckCraft draws, so PowerPoint opens it the same way. + let bytes = deckcraft_pptx::export(&s.doc().unwrap().doc).unwrap(); + let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap(); + let mut xml = String::new(); + std::io::Read::read_to_string(&mut z.by_name("ppt/slides/slide1.xml").unwrap(), &mut xml).unwrap(); + let scales: Vec = xml.split("fontScale=\"").skip(1).filter_map(|s| s.split('"').next()?.parse().ok()).collect(); + assert!(scales.iter().any(|v| *v < 100_000), "the fitted scale is saved: {scales:?}"); + // Undo restores the state before the edit, and redo brings the stored fit back. + s.execute("edit.undo", &json!({})).unwrap(); + let sh = s.doc().unwrap().shape(id).cloned().unwrap(); + let Some(AutoFit::Shrink { font_scale, .. }) = sh.text.unwrap().body.autofit else { panic!("shrink") }; + assert_eq!(font_scale, Some(1.0), "undo restores the scale the edit started from"); + s.execute("edit.redo", &json!({})).unwrap(); + let sh = s.doc().unwrap().shape(id).cloned().unwrap(); + let Some(AutoFit::Shrink { font_scale, .. }) = sh.text.unwrap().body.autofit else { panic!("shrink") }; + assert!(font_scale.is_some_and(|v| v < 1.0), "redo brings the stored fit back: {font_scale:?}"); } #[test] diff --git a/crates/format/src/lib.rs b/crates/format/src/lib.rs index 679dfc7f..f1d0dc83 100644 --- a/crates/format/src/lib.rs +++ b/crates/format/src/lib.rs @@ -690,6 +690,29 @@ mod tests { } } + /// A body whose file had no `fontScale` (``) keeps that state through the + /// native format: it is drawn at 100 % and only fitted when its text changes (#724). + #[test] + fn a_bare_normautofit_body_round_trips() { + use deckcraft_model::text::{AutoFit, TextBody}; + let mut p = Presentation::default(); + let mut body = TextBody::from_text("Overflowing"); + body.body.autofit = Some(AutoFit::Shrink { font_scale: None, line_reduction: 0.0 }); + let sh = deckcraft_model::Shape { text: Some(body), ..Default::default() }; + Arc::make_mut(&mut p.slides[0]).shapes.push(sh); + let back = load(&save(&p).unwrap()).unwrap_or_else(|e| panic!("{e}")); + let body = back.slides[0].shapes.last().unwrap().text.clone().unwrap(); + assert_eq!(body.body.autofit, Some(AutoFit::Shrink { font_scale: None, line_reduction: 0.0 })); + // A scale the file saved round-trips as itself. + let mut body = TextBody::from_text("Overflowing"); + body.body.autofit = Some(AutoFit::Shrink { font_scale: Some(0.62), line_reduction: 0.2 }); + let sh = deckcraft_model::Shape { text: Some(body), ..Default::default() }; + Arc::make_mut(&mut p.slides[0]).shapes.push(sh); + let back = load(&save(&p).unwrap()).unwrap_or_else(|e| panic!("{e}")); + let body = back.slides[0].shapes.last().unwrap().text.clone().unwrap(); + assert_eq!(body.body.autofit, Some(AutoFit::Shrink { font_scale: Some(0.62), line_reduction: 0.2 })); + } + /// Only `null`s where numbers belong are patched: other damage still fails. #[test] fn other_damage_is_still_reported() { diff --git a/crates/model/src/defaults.rs b/crates/model/src/defaults.rs index 19adc7bb..af6948ea 100644 --- a/crates/model/src/defaults.rs +++ b/crates/model/src/defaults.rs @@ -203,13 +203,13 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) { let title_box = |b: &mut B| { let (x, y, ww, hh) = r(60.0, 29.0, 840.0, 104.0); let mut s = b.shape("Title 1", PhType::Title, 0, x, y, ww, hh); - s.text = Some(body_with(Some(Anchor::Middle), Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }))); + s.text = Some(body_with(Some(Anchor::Middle), Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }))); s }; let body_box = |b: &mut B, name: &str, idx: u32, x: f64, y: f64, ww: f64, hh: f64| { let (x, y, ww, hh) = r(x, y, ww, hh); let mut s = b.shape(name, PhType::Body, idx, x, y, ww, hh); - s.text = Some(body_with(Some(Anchor::Top), Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }))); + s.text = Some(body_with(Some(Anchor::Top), Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }))); s }; @@ -263,7 +263,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) { t.text = Some(TextBody { body: BodyProps { anchor: Some(Anchor::Bottom), - autofit: Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }), + autofit: Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }), ..Default::default() }, list_style: ls, @@ -288,7 +288,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) { st.text = Some(TextBody { body: BodyProps { anchor: Some(Anchor::Top), - autofit: Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }), + autofit: Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }), ..Default::default() }, list_style: ls, diff --git a/crates/model/src/text.rs b/crates/model/src/text.rs index 1d862b15..b317d3d9 100644 --- a/crates/model/src/text.rs +++ b/crates/model/src/text.rs @@ -41,8 +41,14 @@ impl Anchor { pub enum AutoFit { /// Do not autofit. None, - /// Shrink text on overflow: font scale and line-spacing reduction (0–1). - Shrink { font_scale: f64, line_reduction: f64 }, + /// Shrink text on overflow: font scale and line-spacing reduction (0–1). `font_scale` is + /// `None` for a file's `` with no `fontScale`: the schema default 100 %, + /// which PowerPoint draws as it is and only fits again when the text changes (#724). + Shrink { + #[serde(skip_serializing_if = "Option::is_none")] + font_scale: Option, + line_reduction: f64, + }, /// Resize shape to fit text. Shape, } diff --git a/crates/pptx/src/read/dml.rs b/crates/pptx/src/read/dml.rs index 912a8547..c88bd1a9 100644 --- a/crates/pptx/src/read/dml.rs +++ b/crates/pptx/src/read/dml.rs @@ -338,7 +338,7 @@ pub fn body_pr(b: &El) -> BodyProps { match e.local() { "normAutofit" => { p.autofit = Some(AutoFit::Shrink { - font_scale: pct(e, "fontScale").unwrap_or(1.0).clamp(0.01, 1.0), + font_scale: pct(e, "fontScale").map(|v| v.clamp(0.01, 1.0)), line_reduction: pct(e, "lnSpcReduction").unwrap_or(0.0).clamp(0.0, 1.0), }) } @@ -645,8 +645,14 @@ mod tests { assert_eq!(b.wrap, Some(false)); assert_eq!(b.vert, Some(TextDir::Vertical270)); assert_eq!(b.columns, Some(2)); - assert_eq!(b.autofit, Some(AutoFit::Shrink { font_scale: 0.625, line_reduction: 0.2 })); + assert_eq!(b.autofit, Some(AutoFit::Shrink { font_scale: Some(0.625), line_reduction: 0.2 })); assert_eq!(b.space_first_last, None); + // A bare `` keeps no scale: the schema default 100 % is drawn as it is, + // not the same as a saved 100 % (`fontScale="100000"`) (#724). + let d = parse(br#""#).unwrap(); + assert_eq!(body_pr(&d.root).autofit, Some(AutoFit::Shrink { font_scale: None, line_reduction: 0.0 })); + let d = parse(br#""#).unwrap(); + assert_eq!(body_pr(&d.root).autofit, Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 })); let d = parse(br#""#).unwrap(); assert_eq!(body_pr(&d.root).space_first_last, Some(true)); // mongolianVert is its own direction (PowerPoint runs its lines left to right) and is diff --git a/crates/pptx/src/write/dml.rs b/crates/pptx/src/write/dml.rs index 371bad7e..09b298f8 100644 --- a/crates/pptx/src/write/dml.rs +++ b/crates/pptx/src/write/dml.rs @@ -489,9 +489,11 @@ pub fn body_pr(w: &mut W, b: &BodyProps) { Some(AutoFit::None) => w.empty0("a:noAutofit"), Some(AutoFit::Shape) => w.empty0("a:spAutoFit"), Some(AutoFit::Shrink { font_scale, line_reduction }) => { - let fs = pct(font_scale.clamp(0.01, 1.0)); + // A scale the model holds is written, 100 % included: reopening then keeps the same + // "draw the saved scale" state instead of becoming a bare `` (#724). + let fs = font_scale.map(|s| pct(s.clamp(0.01, 1.0))); let lr = pct(line_reduction.clamp(0.0, 1.0)); - w.empty("a:normAutofit", A::new().o("fontScale", (fs != 100_000).then_some(fs)).o("lnSpcReduction", (lr != 0).then_some(lr))); + w.empty("a:normAutofit", A::new().o("fontScale", fs).o("lnSpcReduction", (lr != 0).then_some(lr))); } None => {} } diff --git a/crates/pptx/tests/corpus_charts.rs b/crates/pptx/tests/corpus_charts.rs index ee3faffd..ff1e6424 100644 --- a/crates/pptx/tests/corpus_charts.rs +++ b/crates/pptx/tests/corpus_charts.rs @@ -109,7 +109,7 @@ fn corpus() -> Presentation { let mut columns = TextBody::from_text("Two columns\nof vertical text"); columns.body.columns = Some(3); columns.body.col_spacing = Some(18.0); - columns.body.autofit = Some(AutoFit::Shrink { font_scale: 0.85, line_reduction: 0.2 }); + columns.body.autofit = Some(AutoFit::Shrink { font_scale: Some(0.85), line_reduction: 0.2 }); columns.body.vert = Some(TextDir::Vertical270); let mut col = shape(&mut p, "Columns", 480.0, 40.0, 200.0, 160.0); col.text = Some(columns); @@ -176,7 +176,7 @@ fn chart_table_and_text_corpus_round_trips() { assert_eq!(plain(cols), "Two columns\nof vertical text"); assert_eq!(cols.body.columns, Some(3)); assert_eq!(cols.body.col_spacing, Some(18.0)); - assert_eq!(cols.body.autofit, Some(AutoFit::Shrink { font_scale: 0.85, line_reduction: 0.2 })); + assert_eq!(cols.body.autofit, Some(AutoFit::Shrink { font_scale: Some(0.85), line_reduction: 0.2 })); assert_eq!(cols.body.vert, Some(TextDir::Vertical270)); let warp = by_name(slide, "Warp").text.as_ref().expect("warp"); diff --git a/crates/pptx/tests/corpus_style.rs b/crates/pptx/tests/corpus_style.rs index 65c3631c..d552002f 100644 --- a/crates/pptx/tests/corpus_style.rs +++ b/crates/pptx/tests/corpus_style.rs @@ -79,7 +79,7 @@ fn style_deck() -> Presentation { let mut word = shape(&mut p, "Styled text", 40.0, 40.0, 400.0, 180.0); word.text = Some(TextBody { body: BodyProps { - autofit: Some(AutoFit::Shrink { font_scale: 0.8, line_reduction: 0.2 }), + autofit: Some(AutoFit::Shrink { font_scale: Some(0.8), line_reduction: 0.2 }), columns: Some(2), col_spacing: Some(12.0), vert: Some(TextDir::Vertical270), @@ -167,7 +167,7 @@ fn style_kinds_round_trip() { let twice = round(&once); let body = &by_name(&once, "Styled text").text.as_ref().expect("text").body; - assert_eq!(body.autofit, Some(AutoFit::Shrink { font_scale: 0.8, line_reduction: 0.2 })); + assert_eq!(body.autofit, Some(AutoFit::Shrink { font_scale: Some(0.8), line_reduction: 0.2 })); assert_eq!(body.columns, Some(2)); assert_eq!(body.col_spacing, Some(12.0)); assert_eq!(body.vert, Some(TextDir::Vertical270)); diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 840b7834..38f0a7b4 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -1549,18 +1549,21 @@ pub fn layout(ctx: &Ctx, shape: &Shape, body: &TextBody, opts: &Opts) -> TextLay let autofit = bp.autofit.unwrap_or(AutoFit::None); // Shrink on overflow starts from the scale the file was saved with (`fontScale`, // `lnSpcReduction`), which is what PowerPoint draws; editing the text resets it. - let (scale0, reduce0) = match (autofit, opts.no_shrink) { + let (scale0, reduce0, no_saved_scale) = match (autofit, opts.no_shrink) { (AutoFit::Shrink { font_scale, line_reduction }, false) => ( - if font_scale.is_finite() { font_scale.clamp(0.1, 1.0) } else { 1.0 }, + font_scale.filter(|s| s.is_finite()).map(|s| s.clamp(0.1, 1.0)).unwrap_or(1.0), if line_reduction.is_finite() { line_reduction.clamp(0.0, 0.2) } else { 0.0 }, + font_scale.is_none(), ), - _ => (1.0, 0.0), + _ => (1.0, 0.0, false), }; let mut l = layout_scaled(ctx, shape, body, &bp, opts, scale0, reduce0); // A scale PowerPoint saved is drawn as it is: PowerPoint fits the text again only when it // changes (and editing resets the saved scale), not when a file opens, even where our // metrics would call it overflowing (trailing empty paragraphs, substituted fonts). - let saved = scale0 < 1.0 || reduce0 > 0.0; + // A body whose file has no `fontScale` at all (``) is the schema default + // 100 % and is drawn as it is too, not refitted on open (#724). + let saved = no_saved_scale || scale0 < 1.0 || reduce0 > 0.0; if let (AutoFit::Shrink { .. }, false) = (autofit, opts.no_shrink) && l.overflow && !saved diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index d647a3e7..9d40d79b 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -186,26 +186,38 @@ fn saved_shrink_scale_is_drawn() { // A file saved with fontScale 62.5% and lnSpcReduction 20% draws at that scale even though // the text would fit at full size, as PowerPoint draws it. let (p, mut sh) = setup("Short", 600.0, 300.0); - sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: 0.625, line_reduction: 0.2 }); + sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: Some(0.625), line_reduction: 0.2 }); let l = lay(&p, &sh, 600.0, 300.0); assert_eq!((l.font_scale, l.line_reduction), (0.625, 0.2)); // Text our metrics find overflowing at the saved scale is still drawn at it, as PowerPoint // draws it until the text changes; with no saved scale it shrinks to fit. let text = (0..30).map(|i| format!("Line number {i}")).collect::>().join("\n"); let (p, mut sh) = setup(&text, 600.0, 200.0); - sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: 0.9, line_reduction: 0.1 }); + sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: Some(0.9), line_reduction: 0.1 }); let l = lay(&p, &sh, 600.0, 200.0); assert_eq!((l.font_scale, l.line_reduction), (0.9, 0.1)); - sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }); + sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }); let l = lay(&p, &sh, 600.0, 200.0); assert!(l.font_scale < 0.9, "{}", l.font_scale); // Nonsense saved values are ignored. let (p, mut sh) = setup("Short", 600.0, 300.0); - sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: f64::NAN, line_reduction: 5.0 }); + sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: Some(f64::NAN), line_reduction: 5.0 }); let l = lay(&p, &sh, 600.0, 300.0); assert_eq!((l.font_scale, l.line_reduction), (1.0, 0.2)); } +/// A body whose file has `` with no `fontScale` is the schema default 100 %: +/// PowerPoint draws it as it is, even overflowing. DeckCraft draws it the same (#724). +#[test] +fn bare_normautofit_draws_at_full_size() { + let text = (0..30).map(|i| format!("Line number {i}")).collect::>().join("\n"); + let (p, mut sh) = setup(&text, 600.0, 200.0); + sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: None, line_reduction: 0.0 }); + let l = lay(&p, &sh, 600.0, 200.0); + assert_eq!(l.font_scale, 1.0, "a body with no saved fontScale draws at 100%"); + assert!(l.overflow, "it overflows, as PowerPoint draws it"); +} + #[test] fn exact_line_spacing_ends_at_the_last_descent() { // 12 pt text at exactly 26.59 pt per line (a table cell PowerPoint sizes to 24.3 pt of text):