Skip to main content

oxml_drawing/
lib.rs

1pub mod color;
2pub mod effect;
3pub mod fill;
4pub mod geometry;
5pub mod line;
6pub mod namespace;
7pub mod order;
8mod preset_shape_data;
9pub mod shape_props;
10pub mod style_ref;
11pub mod table;
12pub mod text;
13pub mod theme;
14pub mod xfrm;
15
16#[cfg(test)]
17mod table_gate_tests {
18    use crate::table::CT_Table;
19
20    #[test]
21    fn merged_table_round_trips_with_merge_origins_intact() {
22        let xml = br#"<a:tbl xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:tblGrid><a:gridCol w="1000"/><a:gridCol w="2000"/><a:gridCol w="3000"/></a:tblGrid><a:tr h="4000"><a:tc rowSpan="2" gridSpan="2"><a:txBody><a:bodyPr/><a:lstStyle/><a:p/></a:txBody><a:tcPr/></a:tc><a:tc hMerge="1"><a:tcPr/></a:tc><a:tc><a:tcPr/></a:tc></a:tr><a:tr h="5000"><a:tc vMerge="1"><a:tcPr/></a:tc><a:tc hMerge="1" vMerge="1"><a:tcPr/></a:tc><a:tc><a:tcPr/></a:tc></a:tr></a:tbl>"#;
23        let table = CT_Table::from_xml(xml).unwrap();
24        assert_eq!(
25            table
26                .grid
27                .columns
28                .iter()
29                .map(|width| width.0)
30                .collect::<Vec<_>>(),
31            vec![1000, 2000, 3000]
32        );
33        assert_eq!(table.rows[0].height.0, 4000);
34        assert_eq!(table.rows[1].height.0, 5000);
35        let origin = &table.rows[0].cells[0];
36        assert_eq!((origin.row_span, origin.grid_span), (2, 2));
37        assert!(!origin.horizontal_merge && !origin.vertical_merge);
38        assert!(table.rows[0].cells[1].horizontal_merge);
39        assert!(table.rows[1].cells[0].vertical_merge);
40        assert!(table.rows[1].cells[1].horizontal_merge);
41        assert!(table.rows[1].cells[1].vertical_merge);
42        let written = table.to_xml().unwrap();
43        assert_eq!(table, CT_Table::from_xml(&written).unwrap());
44    }
45}
46
47#[cfg(test)]
48mod tests {
49
50    /// F-X024. The shared crates must not depend on a format crate.
51    ///
52    /// `oxml-drawing` used to be the one exception, hosting the Word theme
53    /// adapter and therefore depending on `rdocx-oxml`. That single edge made
54    /// the two publication trains mutually dependent, so neither could publish
55    /// first once both carried breaking changes. The adapter now lives in
56    /// `rdocx-oxml` and the rule has no exception, which this test keeps true.
57    #[test]
58    fn no_shared_crate_depends_on_a_format_crate() {
59        let manifests: [(&str, &str); 9] = [
60            ("oxml-core", include_str!("../../oxml-core/Cargo.toml")),
61            ("oxml-opc", include_str!("../../oxml-opc/Cargo.toml")),
62            ("oxml-media", include_str!("../../oxml-media/Cargo.toml")),
63            ("oxml-layout", include_str!("../../oxml-layout/Cargo.toml")),
64            ("oxml-drawing", include_str!("../Cargo.toml")),
65            ("oxml-pdf", include_str!("../../oxml-pdf/Cargo.toml")),
66            ("oxml-sml", include_str!("../../oxml-sml/Cargo.toml")),
67            (
68                "oxml-cli-support",
69                include_str!("../../oxml-cli-support/Cargo.toml"),
70            ),
71            (
72                "oxml-py-support",
73                include_str!("../../oxml-py-support/Cargo.toml"),
74            ),
75        ];
76
77        for (crate_name, manifest) in manifests {
78            for line in manifest.lines() {
79                let line = line.trim();
80                if line.starts_with('#') || line.is_empty() {
81                    continue;
82                }
83                // A dependency entry names the crate at the start of the line,
84                // either as `rdocx-oxml.workspace = true` or as a table key.
85                let names_format_crate = line.starts_with("rdocx")
86                    || line.starts_with("rpptx")
87                    || line.starts_with("\"rdocx")
88                    || line.starts_with("\"rpptx");
89                assert!(
90                    !names_format_crate,
91                    "{crate_name} depends on a format crate: {line}"
92                );
93            }
94        }
95    }
96    use std::collections::HashSet;
97    use std::path::PathBuf;
98    use std::process::Command;
99
100    use quick_xml::Reader;
101    use quick_xml::events::Event;
102
103    use super::geometry::CT_CustomGeometry2D;
104    use super::namespace::{A_NS, A_PREFIX, PIC_NS, PIC_PREFIX};
105    use super::preset_shape_data::preset_shape_definition;
106
107    fn workspace_file(path: &str) -> PathBuf {
108        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
109            .join("../..")
110            .join(path)
111    }
112
113    fn run_generator(arguments: &[&str]) -> std::process::Output {
114        Command::new("python3")
115            .arg(workspace_file("tools/gen-presets/generate.py"))
116            .args(arguments)
117            .current_dir(workspace_file("."))
118            .output()
119            .expect("run the preset shape generator")
120    }
121
122    #[test]
123    fn generator_reproduces_checked_in_table() {
124        let output = run_generator(&["--check"]);
125        assert!(
126            output.status.success(),
127            "generator check failed: {}",
128            String::from_utf8_lossy(&output.stderr)
129        );
130    }
131
132    #[test]
133    fn generated_table_covers_every_corpus_preset() {
134        let corpus = std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
135            .map(PathBuf::from)
136            .unwrap_or_else(|| workspace_file("corpus/pptx"));
137        if !corpus.is_dir() {
138            assert_ne!(
139                std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
140                Some(std::ffi::OsStr::new("1")),
141                "the pinned corpus is required but {} does not exist",
142                corpus.display()
143            );
144            eprintln!("corpus gate skipped because {} is absent", corpus.display());
145            return;
146        }
147        let corpus = corpus.to_str().expect("corpus path is UTF-8");
148        let output = run_generator(&["--check", "--corpus", corpus]);
149        assert!(
150            output.status.success(),
151            "corpus preset check failed: {}",
152            String::from_utf8_lossy(&output.stderr)
153        );
154    }
155
156    #[test]
157    fn source_has_187_direct_definitions() {
158        let source = std::fs::read(workspace_file(
159            "tools/gen-presets/presetShapeDefinitions.xml",
160        ))
161        .expect("read the pinned official preset source");
162        let mut reader = Reader::from_reader(source.as_slice());
163        let mut buffer = Vec::new();
164        let mut depth = 0usize;
165        let mut direct_definitions = 0usize;
166        let mut names = HashSet::new();
167        loop {
168            match reader
169                .read_event_into(&mut buffer)
170                .expect("parse source XML")
171            {
172                Event::Start(element) => {
173                    if depth == 1 {
174                        direct_definitions += 1;
175                        names.insert(element.name().as_ref().to_vec());
176                    }
177                    depth += 1;
178                }
179                Event::Empty(element) if depth == 1 => {
180                    direct_definitions += 1;
181                    names.insert(element.name().as_ref().to_vec());
182                }
183                Event::End(_) => depth -= 1,
184                Event::Eof => break,
185                _ => {}
186            }
187            buffer.clear();
188        }
189        assert_eq!(direct_definitions, 187);
190        assert_eq!(names.len(), 186);
191        let source = String::from_utf8(source).expect("source XML is UTF-8");
192        let duplicates: Vec<_> = source
193            .split("<upDownArrow>")
194            .skip(1)
195            .map(|suffix| {
196                suffix
197                    .split("</upDownArrow>")
198                    .next()
199                    .expect("complete upDownArrow definition")
200            })
201            .collect();
202        assert_eq!(duplicates.len(), 2);
203        assert_eq!(duplicates[0].as_bytes(), duplicates[1].as_bytes());
204    }
205
206    #[test]
207    fn generated_lookup_has_known_and_unknown_cases() {
208        let rectangle = preset_shape_definition("rect").expect("known rectangle preset");
209        assert!(CT_CustomGeometry2D::from_xml(rectangle).is_ok());
210        assert!(preset_shape_definition("notAStandardPreset").is_none());
211    }
212
213    #[test]
214    fn drawingml_namespace_uris_match_the_specification() {
215        assert_eq!(
216            A_NS,
217            "http://schemas.openxmlformats.org/drawingml/2006/main"
218        );
219        assert_eq!(A_PREFIX, "a");
220        assert_eq!(
221            PIC_NS,
222            "http://schemas.openxmlformats.org/drawingml/2006/picture"
223        );
224        assert_eq!(PIC_PREFIX, "pic");
225    }
226
227    #[test]
228    fn oxml_drawing_is_an_explicit_publication_candidate() {
229        let manifest = include_str!("../Cargo.toml");
230        assert!(manifest.contains("version = \"0.3.0\""));
231        assert!(manifest.contains("publish = true"));
232    }
233}