Preserve point-sized bullets in PPTX

This commit is contained in:
Harrison Booth
2026-10-30 22:42:28 -05:00
parent eaeb06c655
commit b1900e526d
5 changed files with 37 additions and 2 deletions
+3
View File
@@ -240,6 +240,8 @@ pub struct ParaProps {
pub bullet_color: Option<ColorRef>,
/// Bullet size as a fraction of the text size.
pub bullet_size: Option<f64>,
/// Absolute bullet font size in points, independent of the paragraph's run size.
pub bullet_size_points: Option<f64>,
pub tabs: Option<Vec<TabStop>>,
pub default_tab: Option<f64>,
pub rtl: Option<bool>,
@@ -270,6 +272,7 @@ impl ParaProps {
self.bullet_color.clone_from(&p.bullet_color);
}
self.bullet_size = self.bullet_size.or(p.bullet_size);
self.bullet_size_points = self.bullet_size_points.or(p.bullet_size_points);
if self.tabs.is_none() {
self.tabs.clone_from(&p.tabs);
}
+4
View File
@@ -433,6 +433,10 @@ pub fn ppr(imp: &mut Imp, part: &Part, p: &El) -> (Option<u8>, ParaProps, Option
"buFont" => pp.bullet_font = e.attr("typeface").map(String::from),
"buClr" => pp.bullet_color = color(e),
"buSzPct" => pp.bullet_size = pct(e, "val").map(|v| v.clamp(0.25, 4.0)),
"buSzPts" => {
pp.bullet_size_points =
e.attr("val").and_then(|v| v.parse::<f64>().ok()).filter(|v| v.is_finite() && *v > 0.0 && *v <= 40_000.0).map(|v| v / 100.0);
}
"tabLst" => {
pp.tabs = Some(
e.children_named("tab")
+4
View File
@@ -535,6 +535,7 @@ pub fn ppr(w: &mut W, x: &mut Exp, o: &mut Out, tag: &str, level: Option<u8>, p:
|| p.bullet_font.is_some()
|| p.bullet_color.is_some()
|| p.bullet_size.is_some()
|| p.bullet_size_points.is_some()
|| p.tabs.is_some()
|| def.is_some();
if !has_kids {
@@ -559,6 +560,9 @@ pub fn ppr(w: &mut W, x: &mut Exp, o: &mut Out, tag: &str, level: Option<u8>, p:
if let Some(s) = p.bullet_size {
w.val("a:buSzPct", pct(s.clamp(0.25, 4.0)));
}
if let Some(s) = p.bullet_size_points {
w.val("a:buSzPts", ((s.clamp(0.01, 400.0) * 100.0).round() as u32).to_string());
}
if let Some(f) = &p.bullet_font {
w.empty("a:buFont", A::new().a("typeface", f));
}
+24
View File
@@ -488,6 +488,30 @@ fn shapes_fills_lines_effects_round_trip() {
assert_eq!(by_name(&q.slides[1], "Group").id, by_name(&p.slides[1], "Group").id);
}
#[test]
fn bullet_size_in_points_survives_two_pptx_round_trips() {
let mut p = Presentation::default();
let mut textbox = shape(&mut p, "Point-sized bullet", 20.0, 20.0, 400.0, 100.0);
textbox.text = Some(TextBody {
paragraphs: vec![Paragraph {
props: deckcraft_model::text::ParaProps {
bullet: Some(Bullet::Char { char: "•".into() }),
bullet_size_points: Some(12.0),
..Default::default()
},
runs: vec![Run::with("Bullet text", RunProps::default())],
..Default::default()
}],
..Default::default()
});
Arc::make_mut(&mut p.slides[0]).shapes.push(textbox);
let q = round(&round(&p));
let props = &by_name(&q.slides[0], "Point-sized bullet").text.as_ref().expect("text").paragraphs[0].props;
assert_eq!(props.bullet_size_points, Some(12.0));
assert_eq!(props.bullet_size, None);
}
#[test]
fn pictures_media_and_ink_round_trip() {
let p = rich_deck();
+2 -2
View File
@@ -76,7 +76,7 @@ pub(crate) fn shape_para(
if let Some(c) = &props.bullet_color {
rp.fill = Some(Fill::solid(c.clone()));
}
rp.size = Some(lead.size.unwrap_or(18.0) * props.bullet_size.unwrap_or(1.0));
rp.size = Some(props.bullet_size_points.unwrap_or(lead.size.unwrap_or(18.0) * props.bullet_size.unwrap_or(1.0)));
rp.underline = None;
rp.strike = None;
rp.highlight = None;
@@ -105,7 +105,7 @@ pub(crate) fn shape_para(
if let Some(c) = &props.bullet_color {
rp.fill = Some(Fill::solid(c.clone()));
}
rp.size = Some(lead.size.unwrap_or(18.0) * props.bullet_size.unwrap_or(1.0));
rp.size = Some(props.bullet_size_points.unwrap_or(lead.size.unwrap_or(18.0) * props.bullet_size.unwrap_or(1.0)));
rp.underline = None;
let mut st = make_style(ctx, &rp, scale, opts.prompt_color);
let label = autonum(scheme, start_at.saturating_sub(1).saturating_add(number.unwrap_or(1)));