diff --git a/assets/bugfixes/issue-371/after.jpg b/assets/bugfixes/issue-371/after.jpg new file mode 100644 index 00000000..4be83c7c Binary files /dev/null and b/assets/bugfixes/issue-371/after.jpg differ diff --git a/assets/bugfixes/issue-371/before.jpg b/assets/bugfixes/issue-371/before.jpg new file mode 100644 index 00000000..cc3a8742 Binary files /dev/null and b/assets/bugfixes/issue-371/before.jpg differ diff --git a/assets/bugfixes/issue-371/gt.jpg b/assets/bugfixes/issue-371/gt.jpg new file mode 100644 index 00000000..80cf7603 Binary files /dev/null and b/assets/bugfixes/issue-371/gt.jpg differ diff --git a/assets/bugfixes/issue-396/compare.jpg b/assets/bugfixes/issue-396/compare.jpg new file mode 100644 index 00000000..e11ee5b1 Binary files /dev/null and b/assets/bugfixes/issue-396/compare.jpg differ diff --git a/crates/office2pdf/src/parser/xlsx_condfmt_tests.rs b/crates/office2pdf/src/parser/xlsx_condfmt_tests.rs index ee546d10..bf86d7a4 100644 --- a/crates/office2pdf/src/parser/xlsx_condfmt_tests.rs +++ b/crates/office2pdf/src/parser/xlsx_condfmt_tests.rs @@ -601,3 +601,106 @@ fn test_cond_fmt_contains_text_background() { "non-matching cell must stay unfilled" ); } + +/// Rewrites self-closing `` and `` children of +/// `` into start/end pairs (``), the form +/// produced by writers such as openpyxl. +fn rewrite_data_bar_children_as_start_end_tags(xlsx_bytes: &[u8]) -> Vec { + let mut archive = + zip::ZipArchive::new(std::io::Cursor::new(xlsx_bytes.to_vec())).expect("read zip"); + let mut out = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); + for i in 0..archive.len() { + let mut file = archive.by_index(i).expect("zip entry"); + let name: String = file.name().to_string(); + let mut content: Vec = Vec::new(); + std::io::Read::read_to_end(&mut file, &mut content).expect("read entry"); + if name.starts_with("xl/worksheets/") { + let xml = String::from_utf8(content).expect("sheet xml utf8"); + let rewritten = regex_like_expand_self_closing(&xml); + content = rewritten.into_bytes(); + } + out.start_file(name, zip::write::FileOptions::default()) + .expect("start entry"); + std::io::Write::write_all(&mut out, &content).expect("write entry"); + } + out.finish().expect("finish zip").into_inner() +} + +/// Expands every self-closing `` and `` into +/// `` / `` without a regex dependency. +fn regex_like_expand_self_closing(xml: &str) -> String { + let mut result = xml.to_string(); + for tag in ["cfvo", "color"] { + let open = format!("<{tag} "); + let mut rewritten = String::with_capacity(result.len()); + let mut rest: &str = &result; + while let Some(start) = rest.find(&open) { + let after_start = &rest[start..]; + let end = after_start.find("/>").expect("self-closing tag"); + rewritten.push_str(&rest[..start]); + rewritten.push_str(&after_start[..end]); + rewritten.push_str(&format!(">")); + rest = &after_start[end + 2..]; + } + rewritten.push_str(rest); + result = rewritten; + } + result +} + +#[test] +fn test_cond_fmt_data_bar_color_from_start_end_tag_children() { + // Real-world dashboards (openpyxl output) write the dataBar rule color + // as ; the bar must render that navy, not + // the default steel blue (issue #371). + let data = build_xlsx_with_cond_fmt(|sheet| { + sheet.get_cell_mut("A1").set_value_number(0.0); + sheet.get_cell_mut("A2").set_value_number(700.0); + sheet.get_cell_mut("A3").set_value_number(1400.0); + + let mut rule = umya_spreadsheet::ConditionalFormattingRule::default(); + rule.set_type(umya_spreadsheet::ConditionalFormatValues::DataBar); + rule.set_priority(1); + + let mut db = umya_spreadsheet::DataBar::default(); + let mut cfvo_min = umya_spreadsheet::ConditionalFormatValueObject::default(); + cfvo_min.set_type(umya_spreadsheet::ConditionalFormatValueObjectValues::Number); + cfvo_min.set_val("0"); + let mut cfvo_max = umya_spreadsheet::ConditionalFormatValueObject::default(); + cfvo_max.set_type(umya_spreadsheet::ConditionalFormatValueObjectValues::Number); + cfvo_max.set_val("1400"); + db.add_cfvo_collection(cfvo_min); + db.add_cfvo_collection(cfvo_max); + let mut bar_color = umya_spreadsheet::Color::default(); + bar_color.set_argb("FF1E2761"); + db.add_color_collection(bar_color); + rule.set_data_bar(db); + + let mut seq = umya_spreadsheet::SequenceOfReferences::default(); + seq.set_sqref("A1:A3"); + let mut cf = umya_spreadsheet::ConditionalFormatting::default(); + cf.set_sequence_of_references(seq); + cf.add_conditional_collection(rule); + sheet.set_conditional_formatting_collection(vec![cf]); + }); + let data = rewrite_data_bar_children_as_start_end_tags(&data); + + let parser = XlsxParser; + let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap(); + let tp = get_sheet_page(&doc, 0); + + let db3 = tp.table.rows[2].cells[0] + .data_bar + .as_ref() + .expect("A3 should have data_bar"); + assert_eq!( + db3.color, + Color::new(0x1E, 0x27, 0x61), + "bar must use the rule color, not the default blue" + ); + assert!( + (db3.fill_pct - 90.0).abs() < 0.01, + "max value should fill maxLength (90%), got {}", + db3.fill_pct + ); +}