diff --git a/crates/xlsx/src/controls.rs b/crates/xlsx/src/controls.rs index dabcadc45..58b240745 100644 --- a/crates/xlsx/src/controls.rs +++ b/crates/xlsx/src/controls.rs @@ -388,6 +388,12 @@ fn shape_number(id: &str) -> Option { id.trim().trim_start_matches("_x0000_s").parse().ok() } +/// A VML shape's number: from its `id` (`_x0000_s1025`), else from `o:spid`, where producers that +/// give the shape a named id (`id="Button 1"`) keep it. +fn number_of(shape: &El) -> Option { + shape.attr("id").and_then(shape_number).or_else(|| shape.attr("spid").and_then(shape_number)) +} + /// Reads a sheet's buttons, scroll bars and spin buttons: each `` with its properties /// part, else its VML shape, and VML shapes the sheet doesn't list (files that only have VML). /// Returns the shape numbers of the controls it read, so pass-through keeps only the others. @@ -398,7 +404,7 @@ pub fn read(cx: &mut Ctx<'_>, controls: Option<&El>, rels: &[Rel], sheet: &mut S None => None, }; let shapes: Vec<&El> = vml.as_ref().map(|v| v.kids("shape").collect()).unwrap_or_default(); - let shape_of = |n: u64| shapes.iter().copied().find(|s| s.attr("id").and_then(shape_number) == Some(n)); + let shape_of = |n: u64| shapes.iter().copied().find(|s| number_of(s) == Some(n)); // Every shape the sheet lists, read here or not. let mut listed: HashSet = HashSet::new(); for ctl in controls.map(controls_in).unwrap_or_default() { @@ -447,7 +453,7 @@ pub fn read(cx: &mut Ctx<'_>, controls: Option<&El>, rels: &[Rel], sheet: &mut S } // Controls known only from the VML. for s in &shapes { - let Some(n) = s.attr("id").and_then(shape_number) else { continue }; + let Some(n) = number_of(s) else { continue }; if listed.contains(&n) || sheet.controls.len() >= MAX_CONTROLS { continue; } diff --git a/crates/xlsx/src/tests/controls_tests.rs b/crates/xlsx/src/tests/controls_tests.rs index 0e52ed3de..a4f97439a 100644 --- a/crates/xlsx/src/tests/controls_tests.rs +++ b/crates/xlsx/src/tests/controls_tests.rs @@ -181,6 +181,35 @@ fn vml_only_controls_are_read_and_clamped() { assert_eq!(c[2].action, Some(ControlAction::Macro { name: "Tidy".into(), input: MacroInput::Selection, output: None })); } +/// Shapes with a named `id` and their number in `o:spid`, as some producers write them. +const VML_SPID: &str = r##" +
Go
1, 0, 6, 0, 3, 0, 7, 0[0]!Tidy
+1, 0, 1, 0, 1, 20, 3, 079 +
"##; + +#[test] +fn vml_shapes_are_also_found_by_their_spid() { + let rels = r#""#; + // The button is listed in without a properties part; the spin button is only in + // the VML. + let bytes = minimal( + r#""#, + &[("xl/worksheets/_rels/sheet1.xml.rels", rels), ("xl/drawings/vmlDrawing1.vml", VML_SPID)], + "", + "", + ); + let (wb, _) = read_xlsx(&bytes).unwrap(); + let c = &wb.sheet(0).unwrap().controls; + assert_eq!(c.len(), 2, "{c:?}"); + assert_eq!((c[0].kind, c[0].name.as_str(), c[0].text.as_str()), (ControlKind::Button, "Go Button", "Go")); + assert_eq!(c[0].action, Some(ControlAction::Macro { name: "Tidy".into(), input: MacroInput::Selection, output: None })); + assert_eq!((c[1].kind, c[1].value, c[1].max), (ControlKind::Spin, 7, 9)); + // Saved again, each is written once and nothing of them is kept twice. + let out = write_xlsx(&wb).unwrap(); + let (back, _) = read_xlsx(&out).unwrap(); + assert_eq!(back.sheet(0).unwrap().controls.len(), 2); +} + #[test] fn the_properties_part_wins_over_the_vml() { let mut wb = book();