Preserve text overflow settings in PPTX

This commit is contained in:
Harrison Booth
2026-11-02 21:45:18 -06:00
parent 8ec4f6cabb
commit 44b133a3c9
8 changed files with 67 additions and 2 deletions
+2
View File
@@ -249,6 +249,8 @@ pub fn body(ctx: &Ctx, shape: &Shape) -> BodyProps {
rtl_col: Some(false),
upright: Some(false),
space_first_last: Some(false),
vert_overflow: None,
horz_overflow: None,
});
b
}
+1
View File
@@ -23,6 +23,7 @@ pub struct Cell {
pub h_merge: bool,
pub v_merge: bool,
pub vertical: Option<crate::text::TextDir>,
pub horz_overflow: Option<crate::text::TextOverflow>,
}
impl Cell {
+31
View File
@@ -121,6 +121,35 @@ pub struct BodyProps {
pub upright: Option<bool>,
/// Space before the first paragraph and after the last count too (`spcFirstLastPara`).
pub space_first_last: Option<bool>,
pub vert_overflow: Option<TextOverflow>,
pub horz_overflow: Option<TextOverflow>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TextOverflow {
Overflow,
Clip,
Ellipsis,
}
impl TextOverflow {
pub fn xml(self) -> &'static str {
match self {
Self::Overflow => "overflow",
Self::Clip => "clip",
Self::Ellipsis => "ellipsis",
}
}
pub fn from_xml(value: &str) -> Option<Self> {
Some(match value {
"overflow" => Self::Overflow,
"clip" => Self::Clip,
"ellipsis" => Self::Ellipsis,
_ => return None,
})
}
}
impl BodyProps {
@@ -144,6 +173,8 @@ impl BodyProps {
self.rtl_col = self.rtl_col.or(p.rtl_col);
self.upright = self.upright.or(p.upright);
self.space_first_last = self.space_first_last.or(p.space_first_last);
self.vert_overflow = self.vert_overflow.or(p.vert_overflow);
self.horz_overflow = self.horz_overflow.or(p.horz_overflow);
}
}
+2
View File
@@ -358,6 +358,8 @@ pub fn body_pr(b: &El) -> BodyProps {
rtl_col: b.bool("rtlCol"),
upright: b.bool("upright"),
space_first_last: b.bool("spcFirstLastPara"),
vert_overflow: b.attr("vertOverflow").and_then(deckcraft_model::text::TextOverflow::from_xml),
horz_overflow: b.attr("horzOverflow").and_then(deckcraft_model::text::TextOverflow::from_xml),
..Default::default()
};
for e in b.elements() {
+1
View File
@@ -805,6 +805,7 @@ fn cell(imp: &mut Imp, part: &Part, tc: &El) -> Cell {
]);
}
c.anchor = pr.attr("anchor").and_then(Anchor::from_xml);
c.horz_overflow = pr.attr("horzOverflow").and_then(deckcraft_model::text::TextOverflow::from_xml);
c.vertical = pr.attr("vert").and_then(TextDir::from_xml);
for (i, n) in ["lnL", "lnR", "lnT", "lnB"].iter().enumerate() {
if let Some(ln) = pr.child(n)
+3 -1
View File
@@ -468,7 +468,9 @@ pub fn body_pr(w: &mut W, b: &BodyProps) {
.o("anchor", b.anchor.map(|a| a.xml()))
.b("anchorCtr", b.anchor_ctr)
.b("upright", b.upright)
.b("spcFirstLastPara", b.space_first_last);
.b("spcFirstLastPara", b.space_first_last)
.o("vertOverflow", b.vert_overflow.map(|v| v.xml()))
.o("horzOverflow", b.horz_overflow.map(|v| v.xml()));
let warp = b.warp.as_deref().filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric()));
if warp.is_none() && b.autofit.is_none() {
w.empty("a:bodyPr", a);
+1
View File
@@ -746,6 +746,7 @@ fn table(w: &mut W, x: &mut Exp, o: &mut Out, t: &Table) {
other => other.xml(),
}),
);
a = a.o("horzOverflow", c.horz_overflow.map(|v| v.xml()));
w.open("a:tcPr", a);
for (tag, l) in [
("a:lnL", &c.borders[0]),
+26 -1
View File
@@ -12,7 +12,7 @@ use deckcraft_model::style::{
};
use deckcraft_model::table::Table;
use deckcraft_model::text::{
Action, Align, Anchor, AutoFit, BodyProps, Bullet, Caps, Hyperlink, Paragraph, Run, RunKind, RunProps, Spacing, Strike, TextBody,
Action, Align, Anchor, AutoFit, BodyProps, Bullet, Caps, Hyperlink, Paragraph, Run, RunKind, RunProps, Spacing, Strike, TextBody, TextOverflow,
};
use deckcraft_model::{
Background, Comment, CustomPath, CustomShow, Geom, InkStroke, MediaClip, Presentation, Section, Shape, ShapeId, ShapeKind, Slide, SlideId,
@@ -468,6 +468,31 @@ fn text_and_formatting_round_trip() {
assert_eq!(run.text, "x=1");
}
#[test]
fn text_overflow_policies_survive_two_pptx_round_trips() {
let mut p = Presentation::default();
let mut slide = new_slide(&mut p, "Text overflow");
let mut text = shape(&mut p, "Text", 10.0, 10.0, 200.0, 80.0);
let mut body = TextBody::from_text("policy");
body.body.vert_overflow = Some(TextOverflow::Clip);
body.body.horz_overflow = Some(TextOverflow::Ellipsis);
text.text = Some(body);
let mut table_shape = shape(&mut p, "Table", 10.0, 100.0, 200.0, 80.0);
let mut table = Table::new(1, 1, 200.0, 80.0);
table.rows[0].cells[0].horz_overflow = Some(TextOverflow::Clip);
table_shape.kind = ShapeKind::Table(table);
slide.shapes = vec![text, table_shape];
p.slides.push(Arc::new(slide));
let twice = round(&round(&p));
let slide = twice.slides.last().expect("added slide");
let text = by_name(slide, "Text").text.as_ref().expect("text body");
assert_eq!(text.body.vert_overflow, Some(TextOverflow::Clip));
assert_eq!(text.body.horz_overflow, Some(TextOverflow::Ellipsis));
let ShapeKind::Table(table) = &by_name(slide, "Table").kind else { panic!("table") };
assert_eq!(table.rows[0].cells[0].horz_overflow, Some(TextOverflow::Clip));
}
#[test]
fn shapes_fills_lines_effects_round_trip() {
let p = rich_deck();