XLSX: form controls are read from the drawing <legacyDrawing r:id> names

The reader took the sheet's first VML drawing relationship. A sheet
with pictures in its header or footer has a second VML drawing
(<legacyDrawingHF>), and when that relationship came first the control
shapes were not found: VML-only controls were skipped, and listed
controls lost their caption and macro. The drawing is now the one
<legacyDrawing r:id> names, as pass-through already does.
This commit is contained in:
prxnvt committed 2026-10-31 16:55:49 -04:00
1 parent 85c22dd27b
commit dd97b29dec
3 files changed
+30 -9

No files matched your search

+3 -2
View File
@@ -397,10 +397,11 @@ fn number_of(shape: &El) -> Option<u64> {
/// Reads a sheet's buttons, scroll bars and spin buttons: each `<control>` with its properties
/// part, else its VML shape, and VML shapes the sheet doesn't list (files that only have VML).
/// The shapes come from the VML drawing with relationship id `vml_rid` (`<legacyDrawing r:id>`).
/// Returns the shape numbers of the controls it read, so pass-through keeps only the others.
pub fn read(cx: &mut Ctx<'_>, controls: Option<&El>, rels: &[Rel], sheet: &mut Sheet) -> HashSet<u64> {
pub fn read(cx: &mut Ctx<'_>, controls: Option<&El>, vml_rid: Option<&str>, rels: &[Rel], sheet: &mut Sheet) -> HashSet<u64> {
let mut read = HashSet::new();
let vml = match rels.iter().find(|r| r.kind == "vmlDrawing") {
let vml = match vml_rid.and_then(|id| rels.iter().find(|r| r.id == id && r.kind == "vmlDrawing")) {
Some(r) => cx.pkg.read_xml(&r.target).ok().flatten(),
None => None,
};
+9 -7
View File
@@ -141,7 +141,6 @@ pub fn read_sheet(cx: &mut Ctx<'_>, part: &str, name: &str) -> Result<(Sheet, Ha
let mut fit_to_page = false;
let mut column_assignments_left = MAX_COLUMN_ASSIGNMENTS;
cx.cf_ext_ids.clear();
let mut controls: Option<&El> = None;
for e in &root.children {
match e.name.as_str() {
"sheetPr" => {
@@ -263,12 +262,11 @@ pub fn read_sheet(cx: &mut Ctx<'_>, part: &str, name: &str) -> Result<(Sheet, Ha
}
}
"extLst" => read_ext(cx, e, &mut sheet),
"controls" | "AlternateContent" => controls = crate::controls::controls_of(e).or(controls),
"protectedRanges" => read_protected_ranges(e, &mut sheet),
_ => {}
}
}
let read_controls = read_form_controls(cx, controls, &rels, &mut sheet);
let read_controls = read_form_controls(cx, &root, &rels, &mut sheet);
// Comments are related to the sheet without an element in it.
for rel in rels.iter().filter(|r| r.kind == "comments") {
if let Some(x) = cx.optional_xml(&rel.target)? {
@@ -293,10 +291,14 @@ pub fn read_sheet(cx: &mut Ctx<'_>, part: &str, name: &str) -> Result<(Sheet, Ha
Ok((sheet, read_controls))
}
/// Buttons, scroll bars and spin buttons: the `<controls>` list with its properties parts, else
/// the VML shapes. Returns the VML shape numbers read, so pass-through keeps only the others.
fn read_form_controls(cx: &mut Ctx<'_>, controls: Option<&El>, rels: &[Rel], sheet: &mut Sheet) -> HashSet<u64> {
if controls.is_some() || rels.iter().any(|r| r.kind == "vmlDrawing") { crate::controls::read(cx, controls, rels, sheet) } else { HashSet::new() }
/// Buttons, scroll bars and spin buttons: the `<controls>` list (also inside
/// `mc:AlternateContent`) with its properties parts, else the shapes of the VML drawing that
/// `<legacyDrawing r:id>` names (not the header and footer drawing of `<legacyDrawingHF>`).
/// Returns the VML shape numbers read, so pass-through keeps only the others.
fn read_form_controls(cx: &mut Ctx<'_>, root: &El, rels: &[Rel], sheet: &mut Sheet) -> HashSet<u64> {
let controls = root.children.iter().filter_map(crate::controls::controls_of).next_back();
let vml = root.child("legacyDrawing").and_then(|e| e.attr("id"));
if controls.is_some() || vml.is_some() { crate::controls::read(cx, controls, vml, rels, sheet) } else { HashSet::new() }
}
/// A protection password hash: the iterated attributes (`algorithmName`, `hashValue`,
+18
View File
@@ -315,3 +315,21 @@ fn the_properties_parts_use_the_ctrl_prop_relationship_type() {
assert_eq!(types.len(), 4, "{rels}");
assert!(types.iter().all(|t| *t == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/ctrlProp"), "{types:?}");
}
/// A sheet with a header and footer drawing and a drawing for its controls: the controls come
/// from the drawing `<legacyDrawing r:id>` names, whichever relationship is listed first.
#[test]
fn controls_come_from_the_legacy_drawing_not_the_header_and_footer_drawing() {
let hf = r##"<xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><v:shape id="LH" o:spid="_x0000_s2049" type="#_x0000_t75"><v:imagedata o:title="logo"/></v:shape></xml>"##;
let rels = r#"<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" Target="../drawings/vmlDrawingHF1.vml"/><Relationship Id="rId9" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" Target="../drawings/vmlDrawing1.vml"/></Relationships>"#;
let bytes = minimal(
r#"<sheetData/><legacyDrawing r:id="rId9"/><legacyDrawingHF r:id="rId1"/>"#,
&[("xl/worksheets/_rels/sheet1.xml.rels", rels), ("xl/drawings/vmlDrawingHF1.vml", hf), ("xl/drawings/vmlDrawing1.vml", VML_ONLY)],
"",
"",
);
let (wb, _) = read_xlsx(&bytes).unwrap();
let c = &wb.sheet(0).unwrap().controls;
assert_eq!(c.len(), 3, "{c:?}");
assert_eq!(c[2].text, "Go");
}