diff --git a/crates/model/src/table.rs b/crates/model/src/table.rs index 00c9cb5..2dd46e3 100644 --- a/crates/model/src/table.rs +++ b/crates/model/src/table.rs @@ -55,6 +55,9 @@ pub struct Table { pub last_col: bool, pub band_row: bool, pub band_col: bool, + /// `a:tblPr` fill: a fill behind the whole table (over the style's own background). + #[serde(skip_serializing_if = "Option::is_none")] + pub background: Option, } impl Default for Table { @@ -69,6 +72,7 @@ impl Default for Table { last_col: false, band_row: true, band_col: false, + background: None, } } } diff --git a/crates/pptx/src/read/shapes.rs b/crates/pptx/src/read/shapes.rs index ed6fd8e..05a7438 100644 --- a/crates/pptx/src/read/shapes.rs +++ b/crates/pptx/src/read/shapes.rs @@ -394,6 +394,7 @@ fn smartart(imp: &mut Imp, part: &Part, gd: &El, frame: &Shape, depth: usize) -> fn table(imp: &mut Imp, part: &Part, tbl: &El) -> Table { let mut t = Table { style: "none".into(), first_row: false, band_row: false, ..Default::default() }; if let Some(pr) = tbl.child("tblPr") { + t.background = pr.elements().find(|e| e.local().ends_with("Fill") && e.local() != "noFill").and_then(|f| dml::fill_el(imp, part, f)); t.first_row = pr.bool("firstRow").unwrap_or(false); t.first_col = pr.bool("firstCol").unwrap_or(false); t.last_row = pr.bool("lastRow").unwrap_or(false); diff --git a/crates/pptx/src/write/shapes.rs b/crates/pptx/src/write/shapes.rs index 9b7309c..f8e415d 100644 --- a/crates/pptx/src/write/shapes.rs +++ b/crates/pptx/src/write/shapes.rs @@ -494,6 +494,9 @@ fn table(w: &mut W, x: &mut Exp, o: &mut Out, t: &Table) { .t("bandRow", t.band_row) .t("bandCol", t.band_col), ); + if let Some(f) = &t.background { + dml::fill(w, x, o, f); + } w.elt("a:tableStyleId", &guid); w.close("a:tblPr"); w.open0("a:tblGrid"); diff --git a/crates/pptx/tests/roundtrip.rs b/crates/pptx/tests/roundtrip.rs index 66576e7..08fcbdc 100644 --- a/crates/pptx/tests/roundtrip.rs +++ b/crates/pptx/tests/roundtrip.rs @@ -780,3 +780,19 @@ fn line_break_options_round_trip() { assert_eq!(props.latin_line_break, Some(true)); assert_eq!(props.hanging_punct, Some(false)); } + +#[test] +fn a_table_fill_round_trips() { + let mut p = defaults::new_presentation(None); + let mut t = Table::new(2, 2, 200.0, 40.0); + t.background = Some(Fill::solid(ColorRef::rgb(Rgba::rgb(0, 112, 192)))); + let mut sh = shape(&mut p, "Filled table", 10.0, 10.0, 200.0, 80.0); + sh.kind = deckcraft_model::ShapeKind::Table(t); + let first = p.slides[0].id; + p.slide_mut(first).expect("slide").shapes.push(sh); + let q = round(&p); + let deckcraft_model::ShapeKind::Table(t) = &by_name(&q.slides[0], "Filled table").kind else { panic!("table") }; + assert!( + matches!(&t.background, Some(Fill::Solid { color }) if matches!(color.base, deckcraft_model::ColorBase::Rgb { rgb } if rgb == Rgba::rgb(0, 112, 192))) + ); +} diff --git a/crates/render/src/table.rs b/crates/render/src/table.rs index 30770da..61eea5b 100644 --- a/crates/render/src/table.rs +++ b/crates/render/src/table.rs @@ -300,13 +300,20 @@ pub fn cell_rects(t: &Table, heights: &[f64]) -> Vec<(usize, usize, Rect)> { pub fn draw(ctx: &mut RenderContext, pres: &Presentation, rctx: &Ctx, t: &Table, m: Affine, _w: f64, _h: f64, fields: &dyn deckcraft_text::Fields) { let _ = pres; let rects = cell_rects(t, &row_heights(rctx, t, fields)); - // A file-defined style's table background, behind every cell. - if let Some(def) = rctx.pres.table_styles.iter().find(|d| d.id.eq_ignore_ascii_case(&t.style)) - && let Some((bg, ph)) = def.background.as_ref().and_then(|b| style_fill(rctx, b)) - && let Some(all) = rects.iter().map(|(_, _, r)| *r).reduce(|a, b| a.union(b)) - { - ctx.set_transform(m); - crate::paint::fill_path(ctx, rctx, &bg, ph, &all.to_path(0.1), all, m, deckcraft_geom::preset::FillMode::Norm, 1.0, rctx.pres); + // Table backgrounds behind every cell: a file-defined style's, then the table's own fill. + let style_bg = rctx + .pres + .table_styles + .iter() + .find(|d| d.id.eq_ignore_ascii_case(&t.style)) + .and_then(|d| d.background.as_ref()) + .and_then(|b| style_fill(rctx, b)); + let own_bg = t.background.clone().map(|f| (f, None)); + if let Some(all) = rects.iter().map(|(_, _, r)| *r).reduce(|a, b| a.union(b)) { + for (bg, ph) in style_bg.into_iter().chain(own_bg) { + ctx.set_transform(m); + crate::paint::fill_path(ctx, rctx, &bg, ph, &all.to_path(0.1), all, m, deckcraft_geom::preset::FillMode::Norm, 1.0, rctx.pres); + } } for (r, c, rect) in &rects { let Some(cell) = t.cell(*r, *c) else { continue }; diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 16b3ccd..5f87689 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -912,3 +912,22 @@ fn a_file_defined_table_style_draws_its_parts() { let left = px(100, 175); assert!(left[2] > 200 && left[0] < 60, "{left:?}"); } + +#[test] +fn a_table_fill_shows_behind_cells_without_their_own() { + // `a:tblPr` solidFill (tdf#127237): cells with no fill show the table's blue. + use deckcraft_model::style::Fill; + let mut t = Table::new(2, 2, 200.0, 50.0); + t.style = "none".into(); + t.background = Some(Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(0, 112, 192)))); + for r in 0..2 { + for c in 0..2 { + t.cell_mut(r, c).unwrap().fill = Some(Fill::None); + } + } + let tbl = Shape { id: ShapeId(795), xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)), kind: ShapeKind::Table(t), ..Default::default() }; + let p = deck_with(vec![tbl]); + let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() }); + assert_eq!(img.pixel(150, 160)[..3], [0, 112, 192]); + assert_eq!(img.pixel(50, 50)[..3], [255, 255, 255]); +}