1use crate::mce::children;
9use crate::model::*;
10use crate::xml::{unescape_attr, Event, Ns, Reader, Start, XmlError};
11
12type XmlResult<T> = Result<T, XmlError>;
13
14#[derive(Clone, Debug, Default, PartialEq, Eq)]
16pub struct SlideReport {
17 pub unknown_graphics: Vec<String>,
19 pub unknown_elements: u32,
21}
22
23impl SlideReport {
24 pub fn is_empty(&self) -> bool {
25 self.unknown_graphics.is_empty() && self.unknown_elements == 0
26 }
27
28 pub fn merge(&mut self, other: &SlideReport) {
29 self.unknown_graphics
30 .extend(other.unknown_graphics.iter().cloned());
31 self.unknown_elements += other.unknown_elements;
32 }
33}
34
35pub fn parse_slide(xml: &[u8]) -> XmlResult<(SlideContent, SlideReport)> {
37 let mut reader = Reader::new(xml);
38 let root = loop {
39 match reader.next()? {
40 Event::Start(start) => break start,
41 Event::Eof => {
42 return Err(XmlError {
43 offset: xml.len(),
44 msg: "no root element",
45 })
46 }
47 _ => {}
48 }
49 };
50 let kind = match root.name.ns {
51 Ns::Pml => SlideKind::from_root(root.name.local),
52 _ => None,
53 };
54 let Some(kind) = kind else {
55 return Err(XmlError {
56 offset: root.offset,
57 msg: "root element is not a slide, layout, master or notes element",
58 });
59 };
60 let show = attr_bool(&reader, &root, Ns::None, b"show").unwrap_or(true);
61 let mut content = SlideContent {
62 kind,
63 name: None,
64 show,
65 shapes: Vec::new(),
66 };
67 let mut report = SlideReport::default();
68 children(&mut reader, &mut |reader, child| {
69 if !child.name.is(Ns::Pml, b"cSld") {
70 return Ok(());
71 }
72 content.name = reader
73 .attr(&child, Ns::None, b"name")
74 .map(unescape_attr)
75 .filter(|name| !name.is_empty());
76 children(reader, &mut |reader, grandchild| {
77 if !grandchild.name.is(Ns::Pml, b"spTree") {
78 return Ok(());
79 }
80 parse_shape_list(reader, &mut content.shapes, &mut report)
81 })
82 })?;
83 Ok((content, report))
84}
85
86fn parse_shape_list<'a>(
87 reader: &mut Reader<'a>,
88 out: &mut Vec<Shape>,
89 report: &mut SlideReport,
90) -> XmlResult<()> {
91 children(reader, &mut |reader, child| {
92 if child.name.ns != Ns::Pml {
93 report.unknown_elements += 1;
94 return Ok(());
95 }
96 let shape = match child.name.local {
97 b"sp" => parse_sp(reader, report)?,
98 b"pic" => parse_pic(reader, report)?,
99 b"grpSp" => parse_grp_sp(reader, report)?,
100 b"graphicFrame" => parse_graphic_frame(reader, report)?,
101 b"cxnSp" => parse_cxn_sp(reader, report)?,
102 b"contentPart" => {
103 let rel = reader.attr(&child, Ns::Rel, b"id").map(unescape_attr);
104 let mut shape = blank_shape(Content::ContentPart(rel));
105 shape.name = String::from("Content Part");
106 shape
107 }
108 _ => return Ok(()),
109 };
110 out.push(shape);
111 Ok(())
112 })
113}
114
115fn blank_shape(content: Content) -> Shape {
116 Shape {
117 id: 0,
118 name: String::new(),
119 hidden: false,
120 description: None,
121 hyperlink: None,
122 placeholder: None,
123 transform: None,
124 text_box: false,
125 content,
126 }
127}
128
129#[derive(Default)]
130struct NonVisual {
131 id: u32,
132 name: String,
133 hidden: bool,
134 description: Option<String>,
135 hyperlink: Option<String>,
136 placeholder: Option<Placeholder>,
137 text_box: bool,
138 media: Option<String>,
139}
140
141impl NonVisual {
142 fn apply(self, shape: &mut Shape) -> Option<String> {
143 shape.id = self.id;
144 shape.name = self.name;
145 shape.hidden = self.hidden;
146 shape.description = self.description;
147 shape.hyperlink = self.hyperlink;
148 shape.placeholder = self.placeholder;
149 shape.text_box = self.text_box;
150 self.media
151 }
152}
153
154fn parse_non_visual<'a>(reader: &mut Reader<'a>, report: &mut SlideReport) -> XmlResult<NonVisual> {
155 let mut nv = NonVisual::default();
156 children(reader, &mut |reader, child| {
157 if child.name.ns != Ns::Pml {
158 report.unknown_elements += 1;
159 return Ok(());
160 }
161 match child.name.local {
162 b"cNvPr" => {
163 nv.id = attr_u32(reader, &child, Ns::None, b"id").unwrap_or(0);
164 nv.name = reader
165 .attr(&child, Ns::None, b"name")
166 .map(unescape_attr)
167 .unwrap_or_default();
168 nv.hidden = attr_bool(reader, &child, Ns::None, b"hidden").unwrap_or(false);
169 nv.description = reader
170 .attr(&child, Ns::None, b"descr")
171 .map(unescape_attr)
172 .filter(|descr| !descr.is_empty());
173 children(reader, &mut |reader, link| {
174 if link.name.is(Ns::Dml, b"hlinkClick") {
175 nv.hyperlink = reader
176 .attr(&link, Ns::Rel, b"id")
177 .map(unescape_attr)
178 .filter(|id| !id.is_empty());
179 }
180 Ok(())
181 })
182 }
183 b"cNvSpPr" => {
184 nv.text_box = attr_bool(reader, &child, Ns::None, b"txBox").unwrap_or(false);
185 Ok(())
186 }
187 b"nvPr" => children(reader, &mut |reader, prop| {
188 match (prop.name.ns, prop.name.local) {
189 (Ns::Pml, b"ph") => {
190 let kind = reader
191 .attr(&prop, Ns::None, b"type")
192 .map(PlaceholderKind::parse)
193 .unwrap_or_default();
194 let idx = attr_u32(reader, &prop, Ns::None, b"idx").unwrap_or(0);
195 nv.placeholder = Some(Placeholder { kind, idx });
196 }
197 (Ns::Dml, b"videoFile" | b"audioFile" | b"quickTimeFile") => {
198 nv.media = reader.attr(&prop, Ns::Rel, b"link").map(unescape_attr);
199 }
200 (Ns::Dml, b"wavAudioFile") => {
201 nv.media = reader.attr(&prop, Ns::Rel, b"embed").map(unescape_attr);
202 }
203 _ => {}
204 }
205 Ok(())
206 }),
207 _ => Ok(()),
208 }
209 })?;
210 Ok(nv)
211}
212
213fn parse_sp<'a>(reader: &mut Reader<'a>, report: &mut SlideReport) -> XmlResult<Shape> {
214 let mut shape = blank_shape(Content::Text(TextBody::default()));
215 let mut body = TextBody::default();
216 children(reader, &mut |reader, child| {
217 if child.name.ns != Ns::Pml {
218 report.unknown_elements += 1;
219 return Ok(());
220 }
221 match child.name.local {
222 b"nvSpPr" => {
223 parse_non_visual(reader, report)?.apply(&mut shape);
224 }
225 b"spPr" => shape.transform = parse_sp_pr(reader)?.0,
226 b"txBody" => body = parse_text_body(reader)?,
227 _ => {}
228 }
229 Ok(())
230 })?;
231 shape.content = Content::Text(body);
232 Ok(shape)
233}
234
235fn parse_pic<'a>(reader: &mut Reader<'a>, report: &mut SlideReport) -> XmlResult<Shape> {
236 let mut shape = blank_shape(Content::Connector);
237 let mut picture = Picture::default();
238 children(reader, &mut |reader, child| {
239 if child.name.ns != Ns::Pml {
240 report.unknown_elements += 1;
241 return Ok(());
242 }
243 match child.name.local {
244 b"nvPicPr" => picture.media = parse_non_visual(reader, report)?.apply(&mut shape),
245 b"blipFill" => parse_blip_fill(reader, &mut picture)?,
246 b"spPr" => shape.transform = parse_sp_pr(reader)?.0,
247 _ => {}
248 }
249 Ok(())
250 })?;
251 shape.content = Content::Picture(picture);
252 Ok(shape)
253}
254
255fn parse_blip_fill<'a>(reader: &mut Reader<'a>, picture: &mut Picture) -> XmlResult<()> {
256 children(reader, &mut |reader, child| {
257 if !child.name.is(Ns::Dml, b"blip") {
258 return Ok(());
259 }
260 picture.embed = reader
261 .attr(&child, Ns::Rel, b"embed")
262 .map(unescape_attr)
263 .filter(|id| !id.is_empty());
264 picture.link = reader
265 .attr(&child, Ns::Rel, b"link")
266 .map(unescape_attr)
267 .filter(|id| !id.is_empty());
268 Ok(())
269 })
270}
271
272fn parse_grp_sp<'a>(reader: &mut Reader<'a>, report: &mut SlideReport) -> XmlResult<Shape> {
273 let mut shape = blank_shape(Content::Connector);
274 let mut shapes = Vec::new();
275 let mut child_space = None;
276 children(reader, &mut |reader, child| {
277 if child.name.ns != Ns::Pml {
278 report.unknown_elements += 1;
279 return Ok(());
280 }
281 match child.name.local {
282 b"nvGrpSpPr" => {
283 parse_non_visual(reader, report)?.apply(&mut shape);
284 }
285 b"grpSpPr" => {
286 let (transform, space) = parse_sp_pr(reader)?;
287 shape.transform = transform;
288 child_space = space;
289 }
290 b"sp" => shapes.push(parse_sp(reader, report)?),
291 b"pic" => shapes.push(parse_pic(reader, report)?),
292 b"grpSp" => shapes.push(parse_grp_sp(reader, report)?),
293 b"graphicFrame" => shapes.push(parse_graphic_frame(reader, report)?),
294 b"cxnSp" => shapes.push(parse_cxn_sp(reader, report)?),
295 b"contentPart" => {
296 let rel = reader.attr(&child, Ns::Rel, b"id").map(unescape_attr);
297 shapes.push(blank_shape(Content::ContentPart(rel)));
298 }
299 _ => {}
300 }
301 Ok(())
302 })?;
303 shape.content = Content::Group(shapes, child_space);
304 Ok(shape)
305}
306
307fn parse_cxn_sp<'a>(reader: &mut Reader<'a>, report: &mut SlideReport) -> XmlResult<Shape> {
308 let mut shape = blank_shape(Content::Connector);
309 children(reader, &mut |reader, child| {
310 if child.name.ns != Ns::Pml {
311 report.unknown_elements += 1;
312 return Ok(());
313 }
314 match child.name.local {
315 b"nvCxnSpPr" => {
316 parse_non_visual(reader, report)?.apply(&mut shape);
317 }
318 b"spPr" => shape.transform = parse_sp_pr(reader)?.0,
319 _ => {}
320 }
321 Ok(())
322 })?;
323 Ok(shape)
324}
325
326fn parse_graphic_frame<'a>(reader: &mut Reader<'a>, report: &mut SlideReport) -> XmlResult<Shape> {
327 let mut shape = blank_shape(Content::UnknownGraphic(String::new()));
328 let mut content = None;
329 children(reader, &mut |reader, child| {
330 match (child.name.ns, child.name.local) {
331 (Ns::Pml, b"nvGraphicFramePr") => {
332 parse_non_visual(reader, report)?.apply(&mut shape);
333 }
334 (Ns::Pml, b"xfrm") => shape.transform = parse_xfrm(reader, &child)?.0,
335 (Ns::Dml, b"graphic") => {
336 children(reader, &mut |reader, data| {
337 if !data.name.is(Ns::Dml, b"graphicData") {
338 return Ok(());
339 }
340 let uri = reader
341 .attr(&data, Ns::None, b"uri")
342 .map(unescape_attr)
343 .unwrap_or_default();
344 content = Some(parse_graphic_data(reader, uri, report)?);
345 Ok(())
346 })?;
347 }
348 (Ns::Pml, _) => {}
349 _ => report.unknown_elements += 1,
350 }
351 Ok(())
352 })?;
353 shape.content = content.unwrap_or(Content::UnknownGraphic(String::new()));
354 Ok(shape)
355}
356
357fn parse_graphic_data<'a>(
358 reader: &mut Reader<'a>,
359 uri: String,
360 report: &mut SlideReport,
361) -> XmlResult<Content> {
362 let mut content = None;
363 children(reader, &mut |reader, child| {
364 let parsed = match (child.name.ns, child.name.local) {
365 (Ns::Dml, b"tbl") => Some(Content::Table(parse_table(reader)?)),
366 (Ns::Chart, b"chart") | (Ns::ChartEx, b"chart") => Some(Content::Chart(
367 reader.attr(&child, Ns::Rel, b"id").map(unescape_attr),
368 )),
369 (Ns::Dgm, b"relIds") => Some(Content::Diagram(
370 reader.attr(&child, Ns::Rel, b"dm").map(unescape_attr),
371 )),
372 (Ns::Pml, b"oleObj") => Some(Content::Ole(parse_ole(reader, &child, report)?)),
373 _ => None,
374 };
375 if parsed.is_some() && content.is_none() {
376 content = parsed;
377 }
378 Ok(())
379 })?;
380 Ok(content.unwrap_or_else(|| {
381 report.unknown_graphics.push(uri.clone());
382 Content::UnknownGraphic(uri)
383 }))
384}
385
386fn parse_ole<'a>(
387 reader: &mut Reader<'a>,
388 start: &Start<'a>,
389 report: &mut SlideReport,
390) -> XmlResult<OleObject> {
391 let mut ole = OleObject {
392 prog_id: reader.attr(start, Ns::None, b"progId").map(unescape_attr),
393 rel_id: reader
394 .attr(start, Ns::Rel, b"id")
395 .map(unescape_attr)
396 .filter(|id| !id.is_empty()),
397 preview: None,
398 };
399 children(reader, &mut |reader, child| {
400 if child.name.is(Ns::Pml, b"pic") {
401 let pic = parse_pic(reader, report)?;
402 if let Content::Picture(picture) = pic.content {
403 ole.preview = Some(picture);
404 }
405 }
406 Ok(())
407 })?;
408 Ok(ole)
409}
410
411fn parse_table<'a>(reader: &mut Reader<'a>) -> XmlResult<Table> {
412 let mut table = Table::default();
413 children(
414 reader,
415 &mut |reader, child| match (child.name.ns, child.name.local) {
416 (Ns::Dml, b"tblGrid") => children(reader, &mut |reader, col| {
417 if col.name.is(Ns::Dml, b"gridCol") {
418 table
419 .column_widths
420 .push(attr_coordinate(reader, &col, Ns::None, b"w").unwrap_or(0));
421 }
422 Ok(())
423 }),
424 (Ns::Dml, b"tr") => {
425 let mut row = Row {
426 height: attr_coordinate(reader, &child, Ns::None, b"h").unwrap_or(0),
427 cells: Vec::new(),
428 };
429 children(reader, &mut |reader, cell| {
430 if !cell.name.is(Ns::Dml, b"tc") {
431 return Ok(());
432 }
433 let mut parsed = Cell {
434 body: TextBody::default(),
435 grid_span: attr_u32(reader, &cell, Ns::None, b"gridSpan").unwrap_or(1),
436 row_span: attr_u32(reader, &cell, Ns::None, b"rowSpan").unwrap_or(1),
437 h_merge: attr_bool(reader, &cell, Ns::None, b"hMerge").unwrap_or(false),
438 v_merge: attr_bool(reader, &cell, Ns::None, b"vMerge").unwrap_or(false),
439 };
440 children(reader, &mut |reader, inner| {
441 if inner.name.is(Ns::Dml, b"txBody") {
442 parsed.body = parse_text_body(reader)?;
443 }
444 Ok(())
445 })?;
446 row.cells.push(parsed);
447 Ok(())
448 })?;
449 table.rows.push(row);
450 Ok(())
451 }
452 _ => Ok(()),
453 },
454 )?;
455 Ok(table)
456}
457
458fn parse_sp_pr<'a>(reader: &mut Reader<'a>) -> XmlResult<(Option<Transform>, Option<ChildSpace>)> {
460 let mut result = (None, None);
461 children(reader, &mut |reader, child| {
462 if child.name.is(Ns::Dml, b"xfrm") {
463 result = parse_xfrm(reader, &child)?;
464 }
465 Ok(())
466 })?;
467 Ok(result)
468}
469
470fn parse_xfrm<'a>(
471 reader: &mut Reader<'a>,
472 start: &Start<'a>,
473) -> XmlResult<(Option<Transform>, Option<ChildSpace>)> {
474 let mut transform = Transform {
475 rot: attr_i64(reader, start, Ns::None, b"rot").unwrap_or(0),
476 flip_h: attr_bool(reader, start, Ns::None, b"flipH").unwrap_or(false),
477 flip_v: attr_bool(reader, start, Ns::None, b"flipV").unwrap_or(false),
478 ..Transform::default()
479 };
480 let mut child_space = ChildSpace::default();
481 let mut has_off_or_ext = false;
482 let mut has_child = false;
483 children(reader, &mut |reader, child| {
484 if child.name.ns != Ns::Dml {
485 return Ok(());
486 }
487 match child.name.local {
488 b"off" => {
489 has_off_or_ext = true;
490 transform.x = attr_coordinate(reader, &child, Ns::None, b"x").unwrap_or(0);
491 transform.y = attr_coordinate(reader, &child, Ns::None, b"y").unwrap_or(0);
492 }
493 b"ext" => {
494 has_off_or_ext = true;
495 transform.cx = attr_coordinate(reader, &child, Ns::None, b"cx").unwrap_or(0);
496 transform.cy = attr_coordinate(reader, &child, Ns::None, b"cy").unwrap_or(0);
497 }
498 b"chOff" => {
499 has_child = true;
500 child_space.x = attr_coordinate(reader, &child, Ns::None, b"x").unwrap_or(0);
501 child_space.y = attr_coordinate(reader, &child, Ns::None, b"y").unwrap_or(0);
502 }
503 b"chExt" => {
504 has_child = true;
505 child_space.cx = attr_coordinate(reader, &child, Ns::None, b"cx").unwrap_or(0);
506 child_space.cy = attr_coordinate(reader, &child, Ns::None, b"cy").unwrap_or(0);
507 }
508 _ => {}
509 }
510 Ok(())
511 })?;
512 Ok((
513 has_off_or_ext.then_some(transform),
514 has_child.then_some(child_space),
515 ))
516}
517
518pub fn parse_text_body<'a>(reader: &mut Reader<'a>) -> XmlResult<TextBody> {
520 let mut body = TextBody::default();
521 children(reader, &mut |reader, child| {
522 if child.name.is(Ns::Dml, b"p") {
523 body.paragraphs.push(parse_paragraph(reader)?);
524 }
525 Ok(())
526 })?;
527 Ok(body)
528}
529
530fn parse_paragraph<'a>(reader: &mut Reader<'a>) -> XmlResult<Paragraph> {
531 let mut paragraph = Paragraph::default();
532 children(reader, &mut |reader, child| {
533 if child.name.ns != Ns::Dml {
534 return Ok(());
535 }
536 match child.name.local {
537 b"pPr" => {
538 paragraph.level =
539 attr_u32(reader, &child, Ns::None, b"lvl").map_or(0, |lvl| lvl.min(8) as u8);
540 children(reader, &mut |reader, prop| {
541 if prop.name.ns != Ns::Dml {
542 return Ok(());
543 }
544 match prop.name.local {
545 b"buNone" => paragraph.bullet = Bullet::None,
546 b"buChar" => {
547 paragraph.bullet = Bullet::Char(
548 reader
549 .attr(&prop, Ns::None, b"char")
550 .map(unescape_attr)
551 .unwrap_or_default(),
552 )
553 }
554 b"buAutoNum" => {
555 paragraph.bullet = Bullet::AutoNumber {
556 scheme: reader
557 .attr(&prop, Ns::None, b"type")
558 .map(unescape_attr)
559 .unwrap_or_default(),
560 start_at: attr_u32(reader, &prop, Ns::None, b"startAt")
561 .unwrap_or(1),
562 }
563 }
564 b"buBlip" => paragraph.bullet = Bullet::Picture,
565 _ => {}
566 }
567 Ok(())
568 })
569 }
570 b"r" => {
571 let mut run = Run {
572 kind: RunKind::Text,
573 text: String::new(),
574 props: RunProps::default(),
575 };
576 parse_run_children(reader, &mut run)?;
577 paragraph.runs.push(run);
578 Ok(())
579 }
580 b"br" => {
581 let mut run = Run {
582 kind: RunKind::LineBreak,
583 text: String::new(),
584 props: RunProps::default(),
585 };
586 parse_run_children(reader, &mut run)?;
587 paragraph.runs.push(run);
588 Ok(())
589 }
590 b"fld" => {
591 let field_type = reader
592 .attr(&child, Ns::None, b"type")
593 .map(unescape_attr)
594 .unwrap_or_default();
595 let mut run = Run {
596 kind: RunKind::Field(field_type),
597 text: String::new(),
598 props: RunProps::default(),
599 };
600 parse_run_children(reader, &mut run)?;
601 paragraph.runs.push(run);
602 Ok(())
603 }
604 _ => Ok(()),
605 }
606 })?;
607 Ok(paragraph)
608}
609
610fn parse_run_children<'a>(reader: &mut Reader<'a>, run: &mut Run) -> XmlResult<()> {
611 children(reader, &mut |reader, child| {
612 if child.name.ns != Ns::Dml {
613 return Ok(());
614 }
615 match child.name.local {
616 b"rPr" => parse_run_props(reader, &child, &mut run.props),
617 b"t" => reader.text_content(&mut run.text),
618 _ => Ok(()),
619 }
620 })
621}
622
623fn parse_run_props<'a>(
624 reader: &mut Reader<'a>,
625 start: &Start<'a>,
626 props: &mut RunProps,
627) -> XmlResult<()> {
628 props.bold = attr_bool(reader, start, Ns::None, b"b");
629 props.italic = attr_bool(reader, start, Ns::None, b"i");
630 props.underline = reader
631 .attr(start, Ns::None, b"u")
632 .map(|value| value != b"none");
633 props.strike = reader
634 .attr(start, Ns::None, b"strike")
635 .map(|value| value != b"noStrike");
636 props.size = attr_u32(reader, start, Ns::None, b"sz");
637 props.lang = reader.attr(start, Ns::None, b"lang").map(unescape_attr);
638 children(reader, &mut |reader, child| {
639 if child.name.ns != Ns::Dml {
640 return Ok(());
641 }
642 match child.name.local {
643 b"hlinkClick" => {
644 props.hyperlink = reader
645 .attr(&child, Ns::Rel, b"id")
646 .map(unescape_attr)
647 .filter(|id| !id.is_empty())
648 }
649 b"latin" => {
650 props.typeface = reader
651 .attr(&child, Ns::None, b"typeface")
652 .map(unescape_attr)
653 }
654 _ => {}
655 }
656 Ok(())
657 })
658}
659
660fn attr_bool(reader: &Reader<'_>, start: &Start<'_>, ns: Ns, local: &[u8]) -> Option<bool> {
661 match reader.attr(start, ns, local)? {
662 b"1" | b"true" | b"on" => Some(true),
663 b"0" | b"false" | b"off" => Some(false),
664 _ => None,
665 }
666}
667
668fn attr_u32(reader: &Reader<'_>, start: &Start<'_>, ns: Ns, local: &[u8]) -> Option<u32> {
669 std::str::from_utf8(reader.attr(start, ns, local)?)
670 .ok()?
671 .trim()
672 .parse()
673 .ok()
674}
675
676fn attr_i64(reader: &Reader<'_>, start: &Start<'_>, ns: Ns, local: &[u8]) -> Option<i64> {
677 std::str::from_utf8(reader.attr(start, ns, local)?)
678 .ok()?
679 .trim()
680 .parse()
681 .ok()
682}
683
684fn attr_coordinate(reader: &Reader<'_>, start: &Start<'_>, ns: Ns, local: &[u8]) -> Option<Emu> {
685 parse_coordinate(reader.attr(start, ns, local)?)
686}
687
688pub fn parse_coordinate(value: &[u8]) -> Option<Emu> {
691 let text = std::str::from_utf8(value).ok()?.trim();
692 if let Ok(emu) = text.parse::<i64>() {
693 return Some(emu);
694 }
695 let unit_start = text.rfind(|c: char| c.is_ascii_digit() || c == '.')? + 1;
696 let (number, unit) = text.split_at(unit_start);
697 let per_unit: f64 = match unit {
698 "in" => 914_400.0,
699 "cm" => 360_000.0,
700 "mm" => 36_000.0,
701 "pt" => 12_700.0,
702 "pc" | "pi" => 152_400.0,
703 _ => return None,
704 };
705 let number: f64 = number.parse().ok()?;
706 Some((number * per_unit).round() as i64)
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712
713 const NS: &str = r#"xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart""#;
714
715 fn slide(body: &str) -> (SlideContent, SlideReport) {
716 let xml = format!(
717 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:sld {NS}><p:cSld name="My Slide"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>{body}</p:spTree></p:cSld></p:sld>"#
718 );
719 parse_slide(xml.as_bytes()).unwrap()
720 }
721
722 #[test]
723 fn a_title_shape_with_runs_breaks_and_fields() {
724 let (content, report) = slide(
725 r#"<p:sp><p:nvSpPr><p:cNvPr id="2" name="Title 1" descr="alt"><a:hlinkClick r:id="rId9"/></p:cNvPr><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="ctrTitle"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm rot="5400000" flipH="1"><a:off x="838200" y="365125"/><a:ext cx="10515600" cy="1325563"/></a:xfrm></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr lvl="1"><a:buChar char="•"/></a:pPr><a:r><a:rPr lang="en-US" b="1" sz="4400" u="sng"><a:latin typeface="Arial"/><a:hlinkClick r:id="rId2"/></a:rPr><a:t>Hello, </a:t></a:r><a:br/><a:fld id="{B6F15528-21DE-4FAA-801E-634DDDAF4B2B}" type="slidenum"><a:rPr lang="en-US"/><a:t>3</a:t></a:fld><a:r><a:t>World & more</a:t></a:r><a:endParaRPr lang="en-US"/></a:p><a:p><a:pPr><a:buNone/></a:pPr></a:p></p:txBody></p:sp>"#,
726 );
727 assert!(report.is_empty());
728 assert_eq!(content.kind, SlideKind::Slide);
729 assert_eq!(content.name.as_deref(), Some("My Slide"));
730 assert!(content.show);
731 assert_eq!(content.shapes.len(), 1);
732 let shape = &content.shapes[0];
733 assert_eq!(shape.id, 2);
734 assert_eq!(shape.name, "Title 1");
735 assert_eq!(shape.description.as_deref(), Some("alt"));
736 assert_eq!(shape.hyperlink.as_deref(), Some("rId9"));
737 assert_eq!(
738 shape.placeholder,
739 Some(Placeholder {
740 kind: PlaceholderKind::CenterTitle,
741 idx: 0
742 })
743 );
744 assert!(shape.is_title());
745 assert_eq!(
746 shape.transform,
747 Some(Transform {
748 x: 838200,
749 y: 365125,
750 cx: 10515600,
751 cy: 1325563,
752 rot: 5400000,
753 flip_h: true,
754 flip_v: false
755 })
756 );
757 let body = shape.text_body().unwrap();
758 assert_eq!(body.paragraphs.len(), 2);
759 let first = &body.paragraphs[0];
760 assert_eq!(first.level, 1);
761 assert_eq!(first.bullet, Bullet::Char("•".into()));
762 assert_eq!(first.runs.len(), 4);
763 assert_eq!(first.runs[0].text, "Hello, ");
764 assert_eq!(
765 first.runs[0].props,
766 RunProps {
767 bold: Some(true),
768 italic: None,
769 underline: Some(true),
770 strike: None,
771 size: Some(4400),
772 hyperlink: Some("rId2".into()),
773 lang: Some("en-US".into()),
774 typeface: Some("Arial".into())
775 }
776 );
777 assert_eq!(first.runs[1].kind, RunKind::LineBreak);
778 assert_eq!(first.runs[2].kind, RunKind::Field("slidenum".into()));
779 assert_eq!(first.runs[2].text, "3");
780 assert_eq!(first.text(), "Hello, \n3World & more");
781 assert_eq!(body.paragraphs[1].bullet, Bullet::None);
782 assert!(body.paragraphs[1].is_empty());
783 assert_eq!(body.text(), "Hello, \n3World & more\n");
784 assert_eq!(content.title().as_deref(), Some("Hello, \n3World & more\n"));
785 }
786
787 #[test]
788 fn pictures_groups_tables_charts_and_unknown_graphics() {
789 let (content, report) = slide(
790 r#"<p:pic><p:nvPicPr><p:cNvPr id="4" name="Picture 3" hidden="1"/><p:cNvPicPr/><p:nvPr><a:videoFile r:link="rId7"/></p:nvPr></p:nvPicPr><p:blipFill><a:blip r:embed="rId3"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="1" y="2"/><a:ext cx="3" cy="4"/></a:xfrm></p:spPr></p:pic>
791<p:grpSp><p:nvGrpSpPr><p:cNvPr id="5" name="Group 4"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="10" y="20"/><a:ext cx="30" cy="40"/><a:chOff x="1" y="2"/><a:chExt cx="3" cy="4"/></a:xfrm></p:grpSpPr><p:sp><p:nvSpPr><p:cNvPr id="6" name="In group"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>inside</a:t></a:r></a:p></p:txBody></p:sp><p:cxnSp><p:nvCxnSpPr><p:cNvPr id="7" name="Connector"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr><p:spPr/></p:cxnSp></p:grpSp>
792<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="8" name="Table 7"/><p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr><p:ph type="tbl" idx="1"/></p:nvPr></p:nvGraphicFramePr><p:xfrm><a:off x="5" y="6"/><a:ext cx="7" cy="8"/></p:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table"><a:tbl><a:tblPr firstRow="1"/><a:tblGrid><a:gridCol w="100"/><a:gridCol w="200"/></a:tblGrid><a:tr h="50"><a:tc gridSpan="2"><a:txBody><a:bodyPr/><a:p><a:r><a:t>Head</a:t></a:r></a:p></a:txBody><a:tcPr/></a:tc><a:tc hMerge="1"><a:txBody><a:bodyPr/><a:p/></a:txBody></a:tc></a:tr><a:tr h="60"><a:tc><a:txBody><a:bodyPr/><a:p><a:r><a:t>a</a:t></a:r></a:p></a:txBody></a:tc><a:tc><a:txBody><a:bodyPr/><a:p><a:r><a:t>b</a:t></a:r></a:p></a:txBody></a:tc></a:tr></a:tbl></a:graphicData></a:graphic></p:graphicFrame>
793<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="9" name="Chart 8"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr><p:xfrm><a:off x="0" y="0"/><a:ext cx="1" cy="1"/></p:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart r:id="rId4"/></a:graphicData></a:graphic></p:graphicFrame>
794<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="10" name="Mystery"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr><p:xfrm><a:off x="0" y="0"/><a:ext cx="1" cy="1"/></p:xfrm><a:graphic><a:graphicData uri="urn:mystery"><a:foo/></a:graphicData></a:graphic></p:graphicFrame>"#,
795 );
796 assert_eq!(report.unknown_graphics, vec!["urn:mystery"]);
797 assert_eq!(report.unknown_elements, 0);
798 assert_eq!(content.shapes.len(), 5);
799 let pic = &content.shapes[0];
800 assert!(pic.hidden);
801 assert_eq!(
802 pic.content,
803 Content::Picture(Picture {
804 embed: Some("rId3".into()),
805 link: None,
806 media: Some("rId7".into())
807 })
808 );
809 assert_eq!(
810 pic.transform,
811 Some(Transform {
812 x: 1,
813 y: 2,
814 cx: 3,
815 cy: 4,
816 rot: 0,
817 flip_h: false,
818 flip_v: false
819 })
820 );
821 let Content::Group(children, space) = &content.shapes[1].content else {
822 panic!()
823 };
824 assert_eq!(children.len(), 2);
825 assert!(children[0].text_box);
826 assert_eq!(children[0].text_body().unwrap().text(), "inside");
827 assert_eq!(children[1].content, Content::Connector);
828 assert_eq!(
829 *space,
830 Some(ChildSpace {
831 x: 1,
832 y: 2,
833 cx: 3,
834 cy: 4
835 })
836 );
837 assert_eq!(
838 content.shapes[1].transform,
839 Some(Transform {
840 x: 10,
841 y: 20,
842 cx: 30,
843 cy: 40,
844 rot: 0,
845 flip_h: false,
846 flip_v: false
847 })
848 );
849 let Content::Table(table) = &content.shapes[2].content else {
850 panic!()
851 };
852 assert_eq!(table.column_widths, vec![100, 200]);
853 assert_eq!(table.rows.len(), 2);
854 assert_eq!(table.rows[0].cells[0].grid_span, 2);
855 assert!(table.rows[0].cells[1].h_merge);
856 assert!(!table.rows[0].cells[1].is_origin());
857 assert_eq!(table.rows[1].cells[1].body.text(), "b");
858 assert_eq!(
859 content.shapes[2].placeholder,
860 Some(Placeholder {
861 kind: PlaceholderKind::Table,
862 idx: 1
863 })
864 );
865 assert_eq!(
866 content.shapes[2].transform,
867 Some(Transform {
868 x: 5,
869 y: 6,
870 cx: 7,
871 cy: 8,
872 rot: 0,
873 flip_h: false,
874 flip_v: false
875 })
876 );
877 assert_eq!(
878 content.shapes[3].content,
879 Content::Chart(Some("rId4".into()))
880 );
881 assert_eq!(
882 content.shapes[4].content,
883 Content::UnknownGraphic("urn:mystery".into())
884 );
885 let walked: Vec<u32> = content.walk().map(|shape| shape.id).collect();
886 assert_eq!(walked, [4, 5, 6, 7, 8, 9, 10]);
887 assert!(content.title().is_none());
888 }
889
890 #[test]
891 fn alternate_content_falls_back_and_unknown_namespaces_are_counted() {
892 let (content, report) = slide(
893 r#"<mc:AlternateContent xmlns:p14="urn:p14"><mc:Choice Requires="p14"><p:sp><p:nvSpPr><p:cNvPr id="2" name="New"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>choice</a:t></a:r></a:p></p:txBody></p:sp></mc:Choice><mc:Fallback><p:sp><p:nvSpPr><p:cNvPr id="2" name="Old"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>fallback</a:t></a:r></a:p></p:txBody></p:sp></mc:Fallback></mc:AlternateContent><x:thing xmlns:x="urn:x"><x:inner/></x:thing><p:extLst><p:ext uri="{1}"><y:z xmlns:y="urn:y"/></p:ext></p:extLst>"#,
894 );
895 assert_eq!(content.shapes.len(), 1);
896 assert_eq!(content.shapes[0].name, "Old");
897 assert_eq!(content.shapes[0].text_body().unwrap().text(), "fallback");
898 assert_eq!(report.unknown_elements, 1);
899 }
900
901 #[test]
902 fn hidden_slides_layouts_and_notes_roots_parse() {
903 let xml = format!(r#"<p:sld {NS} show="0"><p:cSld><p:spTree/></p:cSld></p:sld>"#);
904 let (content, _) = parse_slide(xml.as_bytes()).unwrap();
905 assert!(!content.show);
906 assert!(content.shapes.is_empty());
907 let xml = format!(
908 r#"<p:notes {NS}><p:cSld><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="2" name="Notes Placeholder"/><p:cNvSpPr/><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>speaker notes</a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld></p:notes>"#
909 );
910 let (content, _) = parse_slide(xml.as_bytes()).unwrap();
911 assert_eq!(content.kind, SlideKind::Notes);
912 assert_eq!(
913 content.shapes[0].placeholder.as_ref().unwrap().kind,
914 PlaceholderKind::Body
915 );
916 let xml = format!(
917 r#"<p:sldLayout {NS} type="title"><p:cSld name="Title Slide"><p:spTree/></p:cSld></p:sldLayout>"#
918 );
919 assert_eq!(
920 parse_slide(xml.as_bytes()).unwrap().0.kind,
921 SlideKind::Layout
922 );
923 let strict = r#"<p:sldMaster xmlns:p="http://purl.oclc.org/ooxml/presentationml/main"><p:cSld><p:spTree/></p:cSld></p:sldMaster>"#;
924 assert_eq!(
925 parse_slide(strict.as_bytes()).unwrap().0.kind,
926 SlideKind::Master
927 );
928 let wrong = format!(r#"<p:presentation {NS}/>"#);
929 assert_eq!(
930 parse_slide(wrong.as_bytes()).unwrap_err().msg,
931 "root element is not a slide, layout, master or notes element"
932 );
933 assert!(parse_slide(b"<p:sld").is_err());
934 }
935
936 #[test]
937 fn coordinates_accept_units() {
938 assert_eq!(parse_coordinate(b"914400"), Some(914400));
939 assert_eq!(parse_coordinate(b"-5"), Some(-5));
940 assert_eq!(parse_coordinate(b"1in"), Some(914400));
941 assert_eq!(parse_coordinate(b"2.54cm"), Some(914400));
942 assert_eq!(parse_coordinate(b"72pt"), Some(914400));
943 assert_eq!(parse_coordinate(b"10mm"), Some(360000));
944 assert_eq!(parse_coordinate(b"abc"), None);
945 assert_eq!(parse_coordinate(b"3px"), None);
946 }
947}