Skip to main content

rust_docx/
chart.rs

1use std::borrow::Borrow;
2
3use rust_office_charts::{render_chart, RenderOptions};
4pub use rust_office_charts::{ChartCategory, ChartData, ChartPoint, ChartSeries};
5
6use crate::package::{collect_relationship_targets, relationship_target_from_parts};
7use crate::parse::{svg_data_uri, ParseContext, ParsedStyleSheet};
8use crate::OoxmlPackage;
9
10pub(crate) const CHARTEX_NS: &str = "http://schemas.microsoft.com/office/drawing/2014/chartex";
11
12/// Read the literal and cached series data for the chart part referenced by a figure.
13/// Formula references are not evaluated. A series without cached points remains empty.
14pub fn extract_chart_data(package: &OoxmlPackage, part_name: &str) -> Option<ChartData> {
15    rust_office_charts::parse_chart_xml(&package.parts.get(part_name)?.content)
16}
17
18pub(crate) fn render_chart_part(
19    part_name: &str,
20    context: &ParseContext<'_, impl Borrow<ParsedStyleSheet>>,
21    width: Option<f64>,
22    height: Option<f64>,
23) -> Option<(String, ChartData)> {
24    let parts = context.parts;
25    let part = parts.get(part_name)?;
26    let chart_targets = collect_relationship_targets(parts, part_name);
27    let related = |kind: &str| {
28        let target = chart_targets.get(kind)?.as_ref()?;
29        Some(parts.get(target)?.content.as_str())
30    };
31
32    let package_targets = collect_relationship_targets(parts, "");
33    let main_part = package_targets
34        .get("officeDocument")
35        .and_then(Option::as_deref)
36        .unwrap_or("word/document.xml");
37    let theme_part = relationship_target_from_parts(parts, main_part, "theme");
38    let theme = theme_part
39        .as_ref()
40        .and_then(|name| parts.get(name))
41        .or_else(|| parts.get("word/theme/theme1.xml"));
42    let properties = package_targets
43        .get("extended-properties")
44        .and_then(Option::as_deref)
45        .unwrap_or("docProps/app.xml");
46    let styles = context.styles();
47    let options = RenderOptions {
48        width,
49        height,
50        theme_colors: Some(&styles.theme_colors),
51        font_family: styles.theme_fonts.minor_latin.as_deref(),
52        theme_xml: theme.map(|part| part.content.as_str()),
53        theme_override_xml: related("themeOverride"),
54        style_xml: related("chartStyle"),
55        color_style_xml: related("chartColorStyle"),
56        legacy_defaults: parts
57            .get(properties)
58            .is_some_and(|part| uses_legacy_chart_defaults(&part.content)),
59        ..Default::default()
60    };
61    let chart = render_chart(&part.content, &options)?;
62    Some((svg_data_uri(&chart.svg), chart.data))
63}
64
65fn uses_legacy_chart_defaults(properties: &str) -> bool {
66    let Ok(xml) = roxmltree::Document::parse(properties) else {
67        return false;
68    };
69    let version = xml
70        .root_element()
71        .children()
72        .find(|node| node.is_element() && node.tag_name().name() == "AppVersion")
73        .and_then(|node| node.text())
74        .and_then(|value| value.trim().split('.').next()?.parse::<u16>().ok());
75    matches!(version, Some(1..=12))
76}