//! The reader on small packages written by hand. use craft_opc::{PackageOut, RelsOut}; use craft_pptx::{Defaults, Geom, LayoutType, Master, PhType, Plain, Presentation, ShapeKind, Size}; 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""#; const CT_PRES: &str = "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"; /// A package from (part, content type, xml) and (part, [(kind, target)]) relationships. fn package(parts: &[(&str, &str, String)], rels: &[(&str, &[(&str, &str)])]) -> Vec { let mut out = PackageOut::default(); for (name, ct, xml) in parts { if ct.is_empty() { out.binary(name, "image/png", xml.clone().into_bytes()); } else { out.xml_part(name, ct, xml.clone().into_bytes()); } } for (part, list) in rels { let mut r = RelsOut::default(); for (kind, target) in *list { r.add_new(kind, target, false); } out.rels(part, &r); } out.finish().unwrap() } fn sp(id: u32, name: &str, ph: &str, pr: &str, text: &str) -> String { format!( r#"{ph}{pr}{text}"# ) } const XFRM: &str = r#""#; fn deck() -> Vec { let pres = format!( r#""# ); let master = format!( r#"{}{}"#, sp(2, "Title", r#""#, XFRM, ""), sp(3, "Body", r#""#, XFRM, "") ); let layout = format!( r#"{}"#, sp(2, "Title", r#""#, &format!(r#"{XFRM}"#), "") ); let shapes = [ sp(5, "Title 1", r#""#, "", "Hello"), sp(6, "Box", "", &format!(r#"{XFRM}"#), "Text"), format!(r#"{XFRM}"#), r#"A1B1"#.to_string(), format!(r#"{}"#, sp(10, "Inner", "", XFRM, "In")), r#""#.to_string(), ]; let slide = format!( r#"{}"#, shapes.concat() ); let theme = r#""#; let tags = r#""#; let core = r#"QuarterlyAnn4"#; package( &[ ("ppt/presentation.xml", CT_PRES, pres), ("ppt/slideMasters/slideMaster1.xml", "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml", master), ("ppt/slideLayouts/slideLayout1.xml", "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml", layout), ("ppt/slides/slide1.xml", "application/vnd.openxmlformats-officedocument.presentationml.slide+xml", slide), ("ppt/theme/theme1.xml", "application/vnd.openxmlformats-officedocument.theme+xml", theme.into()), ("ppt/tags/tag1.xml", "application/vnd.openxmlformats-officedocument.presentationml.tags+xml", tags.into()), ("ppt/media/image1.png", "", "png bytes".into()), ("docProps/core.xml", "application/vnd.openxmlformats-package.core-properties+xml", core.into()), ], &[ ( "", &[ ("officeDocument", "ppt/presentation.xml"), ("http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties", "docProps/core.xml"), ], ), ( "ppt/presentation.xml", &[("slideMaster", "slideMasters/slideMaster1.xml"), ("slide", "slides/slide1.xml"), ("slide", "slides/slide2.xml")], ), ("ppt/slideMasters/slideMaster1.xml", &[("slideLayout", "../slideLayouts/slideLayout1.xml"), ("theme", "../theme/theme1.xml")]), ("ppt/slideLayouts/slideLayout1.xml", &[("slideMaster", "../slideMasters/slideMaster1.xml")]), ( "ppt/slides/slide1.xml", &[("slideLayout", "../slideLayouts/slideLayout1.xml"), ("image", "../media/image1.png"), ("tags", "../tags/tag1.xml")], ), ], ) } fn shape<'a>(p: &'a Presentation, name: &str) -> &'a craft_pptx::Shape { let mut found = None; craft_pptx::walk(&p.slides[0].shapes, &mut |s, _| { if s.name == name && found.is_none() { found = Some(s); } }); found.unwrap_or_else(|| panic!("no shape {name}")) } #[test] fn a_small_deck_reads_into_the_model() { let bytes = deck(); assert!(craft_pptx::sniff(&bytes)); let (p, lost) = craft_pptx::import_checked(&bytes, &Plain).unwrap(); assert_eq!(lost, ["ppt/slides/slide2.xml"], "the missing slide is reported and left out"); assert_eq!(p.slide_size, Size::new(720.0, 540.0)); assert_eq!(p.first_slide_number, 3); assert_eq!(p.props.title, "Quarterly"); assert_eq!(p.props.author, "Ann"); assert_eq!(p.props.revision, 4); let m = &p.masters[0]; assert_eq!(m.name, "Main"); assert_eq!(m.theme.name, "Harbour"); assert_eq!(m.theme.fonts.major.latin, "Lato"); assert_eq!(m.title_style.level(0).and_then(|l| l.run.size), Some(44.0)); assert_eq!(m.layouts.len(), 1); assert_eq!(m.layouts[0].kind, LayoutType::Title); let s = &p.slides[0]; assert_eq!(s.layout, m.layouts[0].id); assert!(s.hidden); assert_eq!(s.name, "First"); assert_eq!(s.tags, [("ADDIN".to_string(), "42".to_string())]); assert!(matches!(&s.background, Some(craft_pptx::Background::Fill { .. }))); assert_eq!(p.sections.len(), 1); assert_eq!(p.sections[0].slides, [s.id]); assert_eq!(p.custom_shows[0].name, "Short"); assert_eq!(p.custom_shows[0].slides, [s.id]); assert!(p.validate().is_empty(), "{:?}", p.validate()); assert!(p.next_id >= 11); let (theme, _) = craft_pptx::import_theme(&bytes).unwrap(); assert_eq!(theme.name, "Harbour"); } #[test] fn the_shapes_of_a_small_deck_read_into_the_model() { let p = craft_pptx::import(&deck(), &Plain).unwrap(); // A placeholder with no geometry of its own takes its layout placeholder's. let title = shape(&p, "Title 1"); assert_eq!(title.ph_type(), Some(PhType::CtrTitle)); assert_eq!(title.geom, Geom::preset("ellipse")); assert_eq!(title.text.as_ref().map(|t| t.text()), Some("Hello".into())); let b = shape(&p, "Box"); assert!(matches!(&b.geom, Geom::Preset { name, adj } if name == "roundRect" && adj == &[30000.0])); assert_eq!(b.xfrm.map(|x| (x.x, x.y, x.w, x.h)), Some((1.0, 2.0, 100.0, 50.0))); assert!(b.fill.is_some()); assert_eq!(b.text.as_ref().and_then(|t| t.body.anchor), Some(craft_pptx::text::Anchor::Top), "shape text sits at the top"); let pic = shape(&p, "Picture"); assert_eq!(pic.descr, "A dot"); let ShapeKind::Picture { fill } = &pic.kind else { panic!("not a picture: {:?}", pic.kind) }; let media = p.media(fill.media).unwrap(); assert_eq!((media.name.as_str(), media.content_type.as_str(), media.data.as_slice()), ("image1.png", "image/png", &b"png bytes"[..])); let ShapeKind::Table(t) = &shape(&p, "Table").kind else { panic!("not a table") }; assert_eq!(t.cols, [100.0, 100.0]); assert!(t.first_row); assert_eq!(t.rows[0].cells[1].text.text(), "B1"); let g = shape(&p, "Group"); assert_eq!(g.children().len(), 1); let inner = shape(&p, "Inner").id; let ShapeKind::Connector { start, end } = shape(&p, "Line").kind else { panic!("not a connector") }; assert_eq!(start, Some((b.id, 1))); assert_eq!(end, Some((inner, 3))); } /// A file with no readable master gets the caller's. #[test] fn a_deck_without_a_master_takes_the_callers() { struct Mine; impl Defaults for Mine { fn master(&self, size: Size, after: u32) -> (Master, u32) { let (mut m, last) = Plain.master(size, after); m.name = "Mine".into(); (m, last) } } let pres = format!(r#""#); let slide = format!(r#"{}"#, sp(4, "Box", "", XFRM, "x")); let bytes = package( &[ ("ppt/presentation.xml", CT_PRES, pres), ("ppt/slides/slide1.xml", "application/vnd.openxmlformats-officedocument.presentationml.slide+xml", slide), ], &[("", &[("officeDocument", "ppt/presentation.xml")]), ("ppt/presentation.xml", &[("slide", "slides/slide1.xml")])], ); let p = craft_pptx::import(&bytes, &Mine).unwrap(); assert_eq!(p.masters.len(), 1); assert_eq!(p.masters[0].name, "Mine"); assert_eq!(p.slides[0].layout, p.masters[0].layouts[0].id); assert!(p.validate().is_empty(), "{:?}", p.validate()); } #[test] fn other_files_are_refused() { assert!(!craft_pptx::sniff(b"not a zip")); assert!(craft_pptx::import(b"not a zip", &Plain).is_err()); let doc = package( &[("word/document.xml", "application/xml", "".into())], &[("", &[("officeDocument", "word/document.xml")])], ); assert!(craft_pptx::sniff(&doc), "a package, but not a presentation"); assert!(matches!(craft_pptx::import(&doc, &Plain), Err(craft_pptx::Error::NotPptx(_)))); assert!(craft_pptx::import_theme(&doc).is_err()); } /// Groups nested past 32 levels are cut there; the rest of the slide is read. #[test] fn deep_group_nesting_is_cut() { let open = r#""#; let tree = format!("{}{}{}", open.repeat(40), sp(4, "Deep", "", XFRM, "x"), "".repeat(40)); let pres = format!(r#""#); let slide = format!(r#"{tree}{}"#, sp(5, "Top", "", XFRM, "y")); let bytes = package( &[ ("ppt/presentation.xml", CT_PRES, pres), ("ppt/slides/slide1.xml", "application/vnd.openxmlformats-officedocument.presentationml.slide+xml", slide), ], &[("", &[("officeDocument", "ppt/presentation.xml")]), ("ppt/presentation.xml", &[("slide", "slides/slide1.xml")])], ); let p = craft_pptx::import(&bytes, &Plain).unwrap(); let mut depth = 0; craft_pptx::walk(&p.slides[0].shapes, &mut |_, d| depth = depth.max(d)); assert_eq!(depth, 32, "33 levels of groups, the last one empty"); assert!(p.slides[0].shapes.iter().any(|s| s.name == "Top")); }