Merge pull request #610 from MarekSpodBiedry:issue-550
This commit is contained in:
commit
94ccbcd048
|
|
@ -28,6 +28,8 @@ pub struct Ctx<'a> {
|
|||
pub num_fmts: std::collections::HashMap<u32, String>,
|
||||
/// Value metadata blocks, indexed by the worksheet's one-based `vm` attribute.
|
||||
pub cell_pictures: Vec<Option<Arc<CellPicture>>>,
|
||||
/// x14 conditional-format ids of the current sheet's data bars, with the index of the rule in `cond_formats`.
|
||||
pub cf_ext_ids: Vec<(String, usize)>,
|
||||
}
|
||||
|
||||
impl Ctx<'_> {
|
||||
|
|
@ -79,6 +81,7 @@ pub fn read_xlsx(bytes: &[u8]) -> Result<(Workbook, ReadReport), IoError> {
|
|||
next_id: 1,
|
||||
num_fmts: Default::default(),
|
||||
cell_pictures: vec![],
|
||||
cf_ext_ids: vec![],
|
||||
};
|
||||
let root_rels = cx.pkg.rels("")?;
|
||||
let wb_part = find_rel(&root_rels, "officeDocument").map(|r| r.target.clone()).unwrap_or_else(|| "xl/workbook.xml".into());
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ pub fn read_sheet(cx: &mut Ctx<'_>, part: &str, name: &str) -> Result<Sheet, IoE
|
|||
drop(bytes);
|
||||
|
||||
let mut fit_to_page = false;
|
||||
cx.cf_ext_ids.clear();
|
||||
for e in &root.children {
|
||||
match e.name.as_str() {
|
||||
"sheetPr" => {
|
||||
|
|
@ -596,6 +597,12 @@ fn read_cf(cx: &mut Ctx<'_>, e: &El, sheet: &mut Sheet) {
|
|||
let formulas: Vec<String> = r.kids("formula").map(|f| from_file(f.text.trim())).collect();
|
||||
let f0 = formulas.first().cloned().unwrap_or_default();
|
||||
let text = r.attr("text").unwrap_or("").to_string();
|
||||
// The x14 id that links a data bar to its Excel 2010 extension (solid fill lives there).
|
||||
let ext_id = r
|
||||
.child("extLst")
|
||||
.and_then(|l| l.kids("ext").find_map(|x| x.child("id")))
|
||||
.map(|i| i.text.trim().to_string())
|
||||
.filter(|i| !i.is_empty());
|
||||
let rule = match r.attr("type").unwrap_or("") {
|
||||
"cellIs" => CfRule::CellIs { op: cf_op(r.attr("operator")), a: f0, b: formulas.get(1).cloned(), style },
|
||||
"expression" => CfRule::Expression { formula: f0, style },
|
||||
|
|
@ -649,6 +656,9 @@ fn read_cf(cx: &mut Ctx<'_>, e: &El, sheet: &mut Sheet) {
|
|||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(id) = ext_id {
|
||||
cx.cf_ext_ids.push((id, sheet.cond_formats.len()));
|
||||
}
|
||||
sheet.cond_formats.push(CondFormat {
|
||||
ranges: ranges.clone(),
|
||||
rule,
|
||||
|
|
@ -956,7 +966,23 @@ fn read_ext(cx: &mut Ctx<'_>, e: &El, sheet: &mut Sheet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
"conditionalFormattings" => cx.warn("some Excel 2010 conditional format extensions were skipped"),
|
||||
"conditionalFormattings" => {
|
||||
let mut skipped = false;
|
||||
for r in c.kids("conditionalFormatting").flat_map(|f| f.kids("cfRule")) {
|
||||
let idx = r.attr("id").and_then(|id| cx.cf_ext_ids.iter().find(|(i, _)| i == id)).map(|(_, n)| *n);
|
||||
match (idx.and_then(|n| sheet.cond_formats.get_mut(n)), r.child("dataBar")) {
|
||||
(Some(CondFormat { rule: CfRule::DataBar { gradient, .. }, .. }), Some(db)) => {
|
||||
*gradient = db.flag("gradient", true);
|
||||
// Only the fill style is modelled; borders, lengths, axis or negative colours are lost.
|
||||
skipped |= db.attrs.iter().any(|(k, _)| k != "gradient") || db.children.iter().any(|k| k.name != "cfvo");
|
||||
}
|
||||
_ => skipped = true,
|
||||
}
|
||||
}
|
||||
if skipped {
|
||||
cx.warn("some Excel 2010 conditional format extensions were skipped");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -576,9 +576,33 @@ pub fn write_sheet(wb: &Workbook, si: usize, selected: bool, out: &mut Out) -> (
|
|||
}
|
||||
let _ = write!(s, "<tableParts count=\"{}\">{parts}</tableParts>", tables.len());
|
||||
}
|
||||
// Sparklines (Excel 2010 extension).
|
||||
// Excel 2010 extensions: solid data bars, then sparklines.
|
||||
let solid_bars: Vec<(&CondFormat, u32)> = sheet
|
||||
.cond_formats
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, cf)| !cf.ranges.is_empty() && matches!(cf.rule, CfRule::DataBar { gradient: false, .. }))
|
||||
.map(|(i, cf)| (cf, prio.get(i).copied().unwrap_or(1)))
|
||||
.collect();
|
||||
if !solid_bars.is_empty() || !sheet.sparklines.is_empty() {
|
||||
s.push_str("<extLst>");
|
||||
}
|
||||
if !solid_bars.is_empty() {
|
||||
let _ = write!(
|
||||
s,
|
||||
"<ext uri=\"{{78C0D931-6437-407d-A8A1-0F90B4350D7D}}\" xmlns:x14=\"{X14_NS}\" xmlns:xm=\"{XM_NS}\"><x14:conditionalFormattings>"
|
||||
);
|
||||
for (cf, p) in &solid_bars {
|
||||
if let CfRule::DataBar { min, max, .. } = &cf.rule {
|
||||
let (id, lo, hi) = (solid_bar_id(*p), x14_cfvo(min), x14_cfvo(max));
|
||||
let _ = write!(s, "<x14:conditionalFormatting><x14:cfRule type=\"dataBar\" id=\"{id}\"><x14:dataBar gradient=\"0\">{lo}{hi}");
|
||||
let _ = write!(s, "</x14:dataBar></x14:cfRule><xm:sqref>{}</xm:sqref></x14:conditionalFormatting>", sqref(&cf.ranges));
|
||||
}
|
||||
}
|
||||
s.push_str("</x14:conditionalFormattings></ext>");
|
||||
}
|
||||
if !sheet.sparklines.is_empty() {
|
||||
s.push_str("<extLst><ext uri=\"{05C60535-1F16-4fd2-B633-F4F36F0B64E0}\" xmlns:x14=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main\"><x14:sparklineGroups xmlns:xm=\"http://schemas.microsoft.com/office/excel/2006/main\">");
|
||||
let _ = write!(s, "<ext uri=\"{{05C60535-1F16-4fd2-B633-F4F36F0B64E0}}\" xmlns:x14=\"{X14_NS}\"><x14:sparklineGroups xmlns:xm=\"{XM_NS}\">");
|
||||
for sp in &sheet.sparklines {
|
||||
let ty = match sp.kind {
|
||||
SparklineKind::Line => "",
|
||||
|
|
@ -596,7 +620,10 @@ pub fn write_sheet(wb: &Workbook, si: usize, selected: bool, out: &mut Out) -> (
|
|||
sp.cell.a1()
|
||||
);
|
||||
}
|
||||
s.push_str("</x14:sparklineGroups></ext></extLst>");
|
||||
s.push_str("</x14:sparklineGroups></ext>");
|
||||
}
|
||||
if !solid_bars.is_empty() || !sheet.sparklines.is_empty() {
|
||||
s.push_str("</extLst>");
|
||||
}
|
||||
s.push_str("</worksheet>");
|
||||
(s, rels)
|
||||
|
|
@ -717,6 +744,33 @@ fn quote(t: &str) -> String {
|
|||
format!("\"{}\"", t.replace('"', "\"\""))
|
||||
}
|
||||
|
||||
const X14_NS: &str = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main";
|
||||
const XM_NS: &str = "http://schemas.microsoft.com/office/excel/2006/main";
|
||||
|
||||
/// The id linking a solid data bar's `cfRule` to its x14 extension; unique per sheet because priorities are.
|
||||
fn solid_bar_id(priority: u32) -> String {
|
||||
format!("{{00000000-0000-0000-0000-{priority:012}}}")
|
||||
}
|
||||
|
||||
/// The `cfRule` extension that points a solid data bar at its x14 rule.
|
||||
fn solid_bar_ext(priority: u32) -> String {
|
||||
let id = solid_bar_id(priority);
|
||||
format!("<extLst><ext uri=\"{{B025F937-C7B1-47D3-B67F-A62EFF666E3E}}\" xmlns:x14=\"{X14_NS}\"><x14:id>{id}</x14:id></ext></extLst>")
|
||||
}
|
||||
|
||||
/// An x14 `cfvo`: unlike the 2006 one it has no `val` attribute, the value is an `xm:f` child.
|
||||
fn x14_cfvo(v: &CfValueKind) -> String {
|
||||
let (ty, val) = match v {
|
||||
CfValueKind::Min => return "<x14:cfvo type=\"min\"/>".into(),
|
||||
CfValueKind::Max => return "<x14:cfvo type=\"max\"/>".into(),
|
||||
CfValueKind::Number(n) => ("num", num(*n)),
|
||||
CfValueKind::Percent(n) => ("percent", num(*n)),
|
||||
CfValueKind::Percentile(n) => ("percentile", num(*n)),
|
||||
CfValueKind::Formula(f) => ("formula", text_to_file(f)),
|
||||
};
|
||||
format!("<x14:cfvo type=\"{ty}\"><xm:f>{}</xm:f></x14:cfvo>", esc(&val))
|
||||
}
|
||||
|
||||
fn cf_rule_xml(cf: &CondFormat, priority: u32, out: &mut Out) -> String {
|
||||
let tl = cf.ranges.first().map(|r| r.start.a1()).unwrap_or_else(|| "A1".into());
|
||||
let stop = if cf.stop_if_true { " stopIfTrue=\"1\"" } else { "" };
|
||||
|
|
@ -812,12 +866,13 @@ fn cf_rule_xml(cf: &CondFormat, priority: u32, out: &mut Out) -> String {
|
|||
b.push_str("</colorScale>");
|
||||
format!("<cfRule type=\"colorScale\" priority=\"{priority}\"{stop}>{b}</cfRule>")
|
||||
}
|
||||
CfRule::DataBar { min, max, color, show_value, .. } => format!(
|
||||
"<cfRule type=\"dataBar\" priority=\"{priority}\"{stop}><dataBar{}>{}{}<color {}/></dataBar></cfRule>",
|
||||
CfRule::DataBar { min, max, color, show_value, gradient } => format!(
|
||||
"<cfRule type=\"dataBar\" priority=\"{priority}\"{stop}><dataBar{}>{}{}<color {}/></dataBar>{}</cfRule>",
|
||||
if *show_value { "" } else { " showValue=\"0\"" },
|
||||
cfvo_xml(min),
|
||||
cfvo_xml(max),
|
||||
if *color == Color::Auto { "rgb=\"FF638EC6\"".to_string() } else { color_attrs(color) }
|
||||
if *color == Color::Auto { "rgb=\"FF638EC6\"".to_string() } else { color_attrs(color) },
|
||||
if *gradient { String::new() } else { solid_bar_ext(priority) }
|
||||
),
|
||||
CfRule::IconSet { set, thresholds, reverse, show_value } => {
|
||||
let mut b = format!("<iconSet iconSet=\"{}\"", esc_attr(set));
|
||||
|
|
|
|||
|
|
@ -1052,6 +1052,31 @@ fn package_kind_sets_workbook_content_type() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn solid_data_bar_survives_with_sparklines_and_gradient_neighbours() {
|
||||
let mut wb = Workbook::new();
|
||||
let s = wb.sheet_mut(0).unwrap();
|
||||
s.set_value(at("A1"), Value::Number(1.0));
|
||||
let bar = |gradient: bool, max: CfValueKind| CfRule::DataBar {
|
||||
min: CfValueKind::Min,
|
||||
max,
|
||||
color: Color::Rgb(0x638EC6),
|
||||
gradient,
|
||||
show_value: true,
|
||||
};
|
||||
let rules = [(true, "A1", CfValueKind::Max), (false, "B1", CfValueKind::Formula("$C$1".into())), (false, "D1:D3", CfValueKind::Number(9.0))];
|
||||
for (i, (gradient, range, max)) in rules.into_iter().enumerate() {
|
||||
s.cond_formats.push(CondFormat { ranges: vec![rr(range)], rule: bar(gradient, max), priority: i as u32 + 1, stop_if_true: false });
|
||||
}
|
||||
s.sparklines.push(Sparkline { cell: at("F1"), source: "A1:C1".into(), kind: SparklineKind::Line, color: Color::Rgb(0x376092), markers: false });
|
||||
let bytes = write_xlsx(&wb).unwrap();
|
||||
// x14 cfvo values are `xm:f` children; a `val` attribute there is not valid x14.
|
||||
let (xml, _) = sheet1_xml(&bytes);
|
||||
assert!(xml.contains("<x14:cfvo type=\"num\"><xm:f>9</xm:f></x14:cfvo>"), "{xml}");
|
||||
let (back, rep) = read_xlsx(&bytes).unwrap();
|
||||
assert!(rep.warnings.is_empty(), "{:?}", rep.warnings);
|
||||
let b = &back.sheets[0];
|
||||
assert_eq!(b.cond_formats, wb.sheets[0].cond_formats);
|
||||
assert_eq!(b.sparklines.len(), 1);
|
||||
fn doc_props_keep_boundary_whitespace() {
|
||||
let mut wb = Workbook::new();
|
||||
wb.props = DocProps {
|
||||
|
|
|
|||
Loading…
Reference in New Issue