Merge PR #759 from midasmatterhorn: text: a bare normAutofit draws at 100% and an edit stores the fitted scale (#724)

This commit is contained in:
Brandon Thomas
2026-10-14 21:59:39 -04:00
13 changed files with 169 additions and 30 deletions
+1 -1
View File
@@ -1089,7 +1089,7 @@ fn columns(s: &mut Session, p: &Value) -> Result<Value> {
}
fn autofit(s: &mut Session, p: &Value) -> Result<Value> {
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,
};
+2 -2
View File
@@ -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<Value> {
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() };
+58 -3
View File
@@ -119,9 +119,14 @@ pub(crate) fn edit_text<T>(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<T>(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 `<a:normAutofit/>` (`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<Ctx<'a>> {
match sel.target {
Target::Slides => doc.slides.get(sel.slide).and_then(|s| Ctx::for_slide(doc, s)),
+34 -2
View File
@@ -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::<Vec<_>>().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<i64> = 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]
+23
View File
@@ -690,6 +690,29 @@ mod tests {
}
}
/// A body whose file had no `fontScale` (`<a:normAutofit/>`) 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() {
+4 -4
View File
@@ -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,
+8 -2
View File
@@ -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 `<a:normAutofit/>` 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<f64>,
line_reduction: f64,
},
/// Resize shape to fit text.
Shape,
}
+8 -2
View File
@@ -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 `<a:normAutofit/>` 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#"<a:bodyPr><a:normAutofit/></a:bodyPr>"#).unwrap();
assert_eq!(body_pr(&d.root).autofit, Some(AutoFit::Shrink { font_scale: None, line_reduction: 0.0 }));
let d = parse(br#"<a:bodyPr><a:normAutofit fontScale="100000"/></a:bodyPr>"#).unwrap();
assert_eq!(body_pr(&d.root).autofit, Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }));
let d = parse(br#"<a:bodyPr spcFirstLastPara="1"/>"#).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
+4 -2
View File
@@ -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 `<a:normAutofit/>` (#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 => {}
}
+2 -2
View File
@@ -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");
+2 -2
View File
@@ -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));
+7 -4
View File
@@ -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 (`<a:normAutofit/>`) 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
+16 -4
View File
@@ -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::<Vec<_>>().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 `<a:normAutofit/>` 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::<Vec<_>>().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):