Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_file/tests/pptx.rs

9.4 KiB, 14 runs

created by r1870400018:22886, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
2//! Anthropic Claude
3
4use oxedyne_fe2o3_file::office::deck::{
5 Deck,
6 Slide,
7};
8use oxedyne_fe2o3_file::office::pptx;
9
10use oxedyne_fe2o3_core::{
11 prelude::*,
12 test::test_it,
13};
14use oxedyne_fe2o3_text::doc::{
15 markdown,
16 text_of,
17};
18
19// A `.pptx` LibreOffice wrote: it was handed a deck this crate made and asked to save its own, so
20// the content is known and every byte of the encoding -- and of the skeleton, which is where all the
21// risk lives -- is somebody else's.
22//
23// Its slide relationship ids run `rId3`, `rId4`, `rId5`, which is the case that catches a reader
24// assuming the first relationship is the first slide.
25const FOREIGN: &[u8] = include_bytes!("data/foreign.pptx");
26
27// Prose with three headings, nested bullets and a listing.
28const SOURCE: &str = "\
29# Quarterly Review
30
31An opening line with **bold words** and *emphasis*.
32
33- First finding
34- Second finding
35 - A nested point
36
37## Numbers
38
391. Revenue rose
402. Costs held
41
42```rust
43fn main() {}
44```
45
46## Closing
47
48A final line.
49";
50
51// The parts a `.pptx` cannot open without: the chain a slide hangs from.
52const REQUIRED: [&str; 7] = [
53 "[Content_Types].xml",
54 "_rels/.rels",
55 "ppt/presentation.xml",
56 "ppt/_rels/presentation.xml.rels",
57 "ppt/slideMasters/slideMaster1.xml",
58 "ppt/slideLayouts/slideLayout1.xml",
59 "ppt/theme/theme1.xml",
60];
61
62pub fn test_pptx(filter: &'static str) -> Outcome<()> {
63
64 res!(test_it(filter, &["Every heading starts a slide 000", "all", "pptx"], || {
65 // This split at the SHALLOWEST heading level first, borrowing a rule from `Doc::top_heading`
66 // that is right for finding a title and wrong for ending a slide: the document below came out
67 // as ONE slide holding everything, and the author who wrote three headings expected three.
68 let doc = res!(markdown::parse(SOURCE));
69 let deck = Deck::from_doc(&doc);
70 assert_eq!(deck.slides.len(), 3, "one slide per heading");
71 let titles: Vec<String> = deck.slides.iter()
72 .map(|s| s.title.as_ref().map(|t| text_of(t)).unwrap_or_default())
73 .collect();
74 assert_eq!(titles, vec!["Quarterly Review", "Numbers", "Closing"]);
75 // The nesting of a list becomes the indent level of a bullet, and nothing else does.
76 let levels: Vec<usize> = deck.slides[0].bullets.iter().map(|b| b.level).collect();
77 assert_eq!(levels, vec![0, 0, 0, 1], "got {:?}", levels);
78 // A listing goes on as its lines rather than being dropped: a deck generated from a
79 // technical document is mostly listings.
80 assert!(deck.slides[1].text_of().contains("fn main() {}"), "{}", deck.slides[1].text_of());
81 Ok(())
82 }));
83
84 res!(test_it(filter, &["A created deck carries the chain a slide hangs from 001", "all", "pptx"], || {
85 // The whole of the risk in this format. A slide points at a layout, a layout at a master, a
86 // master at a theme, and PowerPoint refuses a file with any link missing -- with a repair
87 // prompt that names neither the part nor the reason.
88 let doc = res!(markdown::parse(SOURCE));
89 let (bytes, left) = res!(pptx::write(&Deck::from_doc(&doc)));
90 assert!(left.is_empty(), "this source holds no image and no notes");
91 let zip = res!(oxedyne_fe2o3_file::zip::Zip::read(bytes.clone()));
92 for part in REQUIRED {
93 assert!(zip.has(part), "the package is missing {}", part);
94 }
95 for i in 1..=3 {
96 assert!(zip.has(&fmt!("ppt/slides/slide{}.xml", i)));
97 // Each slide names its layout in its OWN rels part. A slide with none is a slide with no
98 // geometry to inherit.
99 assert!(zip.has(&fmt!("ppt/slides/_rels/slide{}.xml.rels", i)));
100 }
101 // The theme must carry three of each style, whether or not anything uses them.
102 let theme = res!(zip.text("ppt/theme/theme1.xml"));
103 for (list, n) in [("a:fillStyleLst", 3), ("a:lnStyleLst", 3), ("a:effectStyleLst", 3)] {
104 let xml = res!(oxedyne_fe2o3_text::xml::Xml::parse(&theme));
105 let found = res!(xml.root()).all(list);
106 assert_eq!(found.len(), 1, "{} appears once", list);
107 assert!(found[0].elems().count() >= n, "{} needs {} entries", list, n);
108 }
109 // And the master must map scheme colours to roles, or the deck comes out white on white.
110 let master = res!(zip.text("ppt/slideMasters/slideMaster1.xml"));
111 assert!(master.contains("p:clrMap"), "the colour map is written explicitly");
112 // Written twice, the same bytes.
113 let (again, _) = res!(pptx::write(&Deck::from_doc(&doc)));
114 assert_eq!(bytes, again);
115 // And it survives the archive's round trip.
116 assert_eq!(res!(zip.write()), bytes);
117 Ok(())
118 }));
119
120 res!(test_it(filter, &["A foreign deck reads back in the right order 002", "all", "pptx"], || {
121 // Slide ORDER lives in `p:sldIdLst`, not in the file names: `slide10.xml` sorts before
122 // `slide2.xml` and is the ninth slide. This fixture's slide relationships start at `rId3`,
123 // because the master and the theme took the first two, so a reader that assumed the first
124 // relationship was the first slide deals the whole deck wrong.
125 let r = res!(pptx::read(FOREIGN));
126 assert_eq!(r.deck.slides.len(), 3);
127 let titles: Vec<String> = r.deck.slides.iter()
128 .map(|s| s.title.as_ref().map(|t| text_of(t)).unwrap_or_default())
129 .collect();
130 assert_eq!(titles, vec!["Quarterly Review", "Numbers", "Closing"], "got {:?}", titles);
131 // A title is a title because of the PLACEHOLDER TYPE on its shape, not because of where it
132 // sits or how large it is.
133 assert!(r.deck.slides[0].title.is_some());
134 let first = r.deck.slides[0].text_of();
135 assert!(first.contains("An opening line"), "{}", first);
136 assert!(first.contains("A nested point"), "{}", first);
137 assert!(r.deck.slides[1].text_of().contains("Revenue rose"));
138 assert!(!r.macros);
139 assert!(r.missing.is_empty());
140 Ok(())
141 }));
142
143 res!(test_it(filter, &["A deck we wrote reads back as what we put in 003", "all", "pptx"], || {
144 let doc = res!(markdown::parse(SOURCE));
145 let deck = Deck::from_doc(&doc);
146 let (bytes, _) = res!(pptx::write(&deck));
147 let back = res!(pptx::read(&bytes));
148 assert_eq!(back.deck.slides.len(), deck.slides.len());
149 for (a, b) in deck.slides.iter().zip(&back.deck.slides) {
150 assert_eq!(text_of(res!(a.title.as_ref().ok_or_else(|| err!("no title"; Missing)))),
151 text_of(res!(b.title.as_ref().ok_or_else(|| err!("no title back"; Missing)))));
152 assert_eq!(a.bullets.len(), b.bullets.len(), "slide bullet count");
153 let levels_a: Vec<usize> = a.bullets.iter().map(|x| x.level).collect();
154 let levels_b: Vec<usize> = b.bullets.iter().map(|x| x.level).collect();
155 assert_eq!(levels_a, levels_b, "the indent levels survive");
156 }
157 Ok(())
158 }));
159
160 res!(test_it(filter, &["What a deck could not carry is counted 004", "all", "pptx"], || {
161 // Speaker's notes need a notes master and a notes layout -- the whole skeleton again, for
162 // content nobody sees on screen. Counted and said rather than written or silently dropped.
163 let mut deck = Deck::new();
164 let mut s = Slide::titled("With notes");
165 s.notes = Some("Remember to pause here.".to_string());
166 deck.slides.push(s);
167 let (_, left) = res!(pptx::write(&deck));
168 assert_eq!(left.notes, 1);
169 assert!(!left.is_empty());
170 // A deck with no slides gets one empty slide: a presentation holding none is a file that
171 // opens and shows nothing, which reads as corruption.
172 let (bytes, _) = res!(pptx::write(&Deck::new()));
173 let back = res!(pptx::read(&bytes));
174 assert_eq!(back.deck.slides.len(), 1);
175 Ok(())
176 }));
177
178 res!(test_it(filter, &["A deck that cannot be read is named 005", "all", "pptx"], || {
179 let ole = [0xD0u8, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1, 0, 0, 0, 0, 0, 0, 0, 0];
180 match pptx::read(&ole) {
181 Err(e) => {
182 let s = fmt!("{}", e);
183 assert!(s.contains("OLE compound file"), "{}", s);
184 }
185 Ok(_) => panic!("an OLE file was read"),
186 }
187 assert!(pptx::read(b"not a deck").is_err());
188 let mut zip = oxedyne_fe2o3_file::zip::Zip::new();
189 zip.set("hello.txt", b"hi".to_vec(), oxedyne_fe2o3_file::zip::Method::Store);
190 assert!(pptx::read(&res!(zip.write())).is_err());
191 Ok(())
192 }));
193
194 res!(test_it(filter, &["A slide layout wears a type the schema has heard of 006", "all", "pptx"], || {
195 // `titleAndBody` stood here and is not one of these. The list is `ST_SlideLayoutType` from
196 // ECMA-376 Part 1 §19.7.15, copied out in full rather than compared against the writer's own
197 // constant -- a test that read the constant would agree with any value the writer chose,
198 // including the wrong one it had. LibreOffice converted the deck without complaint.
199 const LEGAL: [&str; 36] = [
200 "title", "tx", "twoColTx", "tbl", "txAndChart", "chartAndTx", "dgm", "chart",
201 "txAndClipArt", "clipArtAndTx", "titleOnly", "blank", "txAndObj", "objAndTx",
202 "objOnly", "obj", "txAndMedia", "mediaAndTx", "objOverTx", "txOverObj",
203 "txAndTwoObj", "twoObjAndTx", "twoObjOverTx", "fourObj", "vertTx",
204 "clipArtAndVertTx", "vertTitleAndTx", "vertTitleAndTxOverChart", "twoObj",
205 "objAndTwoObj", "twoObjAndObj", "cust", "secHead", "twoTxTwoObj", "objTx", "picTx",
206 ];
207 let doc = res!(markdown::parse(SOURCE));
208 let (bytes, _) = res!(pptx::write(&Deck::from_doc(&doc)));
209 let zip = res!(oxedyne_fe2o3_file::zip::Zip::read(bytes));
210 let xml = res!(zip.text("ppt/slideLayouts/slideLayout1.xml"));
211 let at = res!(xml.find("type=\"").ok_or_else(|| err!(
212 "The slide layout carries no type attribute at all."; Test, Missing))) + 6;
213 let rest = &xml[at..];
214 let kind = res!(rest.split('"').next().ok_or_else(|| err!(
215 "The type attribute is unterminated."; Test, Invalid)));
216 assert!(LEGAL.contains(&kind),
217 "the layout's type=\"{}\" is not one of ST_SlideLayoutType's 36 members", kind);
218 Ok(())
219 }));
220
221 Ok(())
222}