From ea97b18c7073a54fe2adff2e2039c8ebb1da022f Mon Sep 17 00:00:00 2001
From: aiongg <67543387+aiongg@users.noreply.github.com>
Date: Sun, 8 Nov 2026 17:03:31 +0000
Subject: [PATCH] Fix pPTX bar chart: series-level ``
Fixes #1977.
- crates/pptx/src/read/chart.rs
- crates/pptx/tests/chart_data_labels.rs
- crates/pptx/tests/autocraft_regression_1977.rs
---
crates/pptx/src/read/chart.rs | 5 +-
.../pptx/tests/autocraft_regression_1977.rs | 73 +++++++++++++++++++
2 files changed, 77 insertions(+), 1 deletion(-)
create mode 100644 crates/pptx/tests/autocraft_regression_1977.rs
diff --git a/crates/pptx/src/read/chart.rs b/crates/pptx/src/read/chart.rs
index 358f88ff5..3f63709b1 100644
--- a/crates/pptx/src/read/chart.rs
+++ b/crates/pptx/src/read/chart.rs
@@ -300,8 +300,11 @@ fn read_series(s: &El, g: &El, group: (ChartType, bool, Option), categorie
secondary_axis: secondary,
..Default::default()
};
+ // `` hides every label of the series, whatever the group says.
+ if s.path(&["dLbls", "delete"]).map(|v| v.bool("val").unwrap_or(true)) == Some(true) {
+ ser.data_labels = Some(false);
// `` without a val attribute means true, as in the schema's default.
- if let Some(d) = s.child("dLbls").and_then(|d| d.child("showVal")).map(|v| v.bool("val").unwrap_or(true)) {
+ } else if let Some(d) = s.child("dLbls").and_then(|d| d.child("showVal")).map(|v| v.bool("val").unwrap_or(true)) {
ser.data_labels = Some(d);
} else if let Some(false) = group_labels {
// The group asks for no labels and the series does not say otherwise.
diff --git a/crates/pptx/tests/autocraft_regression_1977.rs b/crates/pptx/tests/autocraft_regression_1977.rs
new file mode 100644
index 000000000..6bd2a1dac
--- /dev/null
+++ b/crates/pptx/tests/autocraft_regression_1977.rs
@@ -0,0 +1,73 @@
+//! A series-level `c:dLbls/c:delete val="1"` hides that series' labels although the
+//! chart-wide `showVal` is on (#1977).
+
+use std::io::{Cursor, Read, Write};
+use std::sync::Arc;
+
+use deckcraft_geom::Xfrm;
+use deckcraft_model::chart::{Chart, ChartType};
+use deckcraft_model::{LayoutType, Presentation, Shape, ShapeId, ShapeKind, defaults};
+
+fn bar_chart_pptx() -> Vec {
+ let mut p = defaults::new_presentation(None);
+ p.slides.clear();
+ let layout = defaults::layout_of_kind(&p, LayoutType::Blank).unwrap_or_default();
+ let mut slide = defaults::new_slide(&mut p, layout).expect("slide");
+ let mut shape =
+ Shape { id: ShapeId(p.alloc_id().expect("id")), name: "Bars".into(), xfrm: Some(Xfrm::new(40.0, 40.0, 400.0, 300.0)), ..Default::default() };
+ let mut chart = Chart::sample(ChartType::Column);
+ chart.data_labels = false;
+ for s in &mut chart.series {
+ s.data_labels = None;
+ }
+ shape.kind = ShapeKind::Chart(Box::new(chart));
+ slide.shapes.push(shape);
+ p.slides.push(Arc::new(slide));
+ deckcraft_pptx::export(&p).expect("export")
+}
+
+fn reimport(bytes: &[u8], f: impl Fn(&str) -> String) -> Presentation {
+ let mut zip = zip::ZipArchive::new(Cursor::new(bytes)).expect("zip");
+ let mut out = zip::ZipWriter::new(Cursor::new(Vec::new()));
+ let opts = zip::write::SimpleFileOptions::default();
+ for i in 0..zip.len() {
+ let mut entry = zip.by_index(i).expect("entry");
+ let name = entry.name().to_string();
+ let mut data = vec![];
+ entry.read_to_end(&mut data).expect("read");
+ if name.starts_with("ppt/charts/chart") && name.ends_with(".xml") && !name.contains("_rels") {
+ let xml = String::from_utf8(data).expect("chart xml");
+ data = f(&xml).into_bytes();
+ }
+ out.start_file(&name, opts).expect("start");
+ out.write_all(&data).expect("write");
+ }
+ let patched = out.finish().expect("finish").into_inner();
+ deckcraft_pptx::import(&patched).expect("import")
+}
+
+fn chart_of(p: &Presentation) -> &Chart {
+ let s = p.slides.first().expect("slide");
+ for sh in &s.shapes {
+ if let ShapeKind::Chart(c) = &sh.kind {
+ return c;
+ }
+ }
+ panic!("no chart in the deck");
+}
+
+#[test]
+fn series_dlbls_delete_hides_that_series_labels() {
+ let p = reimport(&bar_chart_pptx(), |xml| {
+ xml.replacen("", r#""#, 1).replacen(
+ "