//! Lock and hidden state (Selection pane): what PowerPoint writes for Lock, and what it reads as locked. use std::io::{Cursor, Read, Write}; use deckcraft_geom::Xfrm; use deckcraft_model::style::PictureFill; use deckcraft_model::table::Table; use deckcraft_model::{Presentation, Shape, ShapeId, ShapeKind, Slide, defaults}; fn png() -> Vec { let img = image::RgbImage::from_pixel(4, 4, image::Rgb([10, 200, 30])); let mut out = Cursor::new(vec![]); image::DynamicImage::ImageRgb8(img).write_to(&mut out, image::ImageFormat::Png).expect("png"); out.into_inner() } fn shape(p: &mut Presentation, name: &str, x: f64) -> Shape { Shape { id: ShapeId(p.alloc_id().expect("ids")), name: name.into(), xfrm: Some(Xfrm::new(x, 40.0, 80.0, 60.0)), ..Default::default() } } /// Slide 1 of a blank deck holds: a locked shape, an unlocked one, a hidden locked text box, a /// locked group (one child unlocked, one hidden), and a locked picture, connector and table. fn deck() -> Presentation { let mut p = defaults::new_presentation(None); let img = p.add_media("image1.png", "image/png", png()).expect("media"); let mut v = vec![]; let mut a = shape(&mut p, "Locked", 0.0); a.locked = true; v.push(a); v.push(shape(&mut p, "Free", 100.0)); let mut t = shape(&mut p, "Hidden text", 200.0); t.text_box = true; t.hidden = true; t.locked = true; v.push(t); let mut c1 = shape(&mut p, "Child locked", 300.0); c1.locked = true; let c2 = shape(&mut p, "Child free", 400.0); let mut c3 = shape(&mut p, "Child hidden", 500.0); c3.hidden = true; let gx = Xfrm::new(300.0, 40.0, 280.0, 60.0); let mut g = Shape { id: ShapeId(p.alloc_id().expect("ids")), name: "Group".into(), xfrm: Some(gx), kind: ShapeKind::Group { children: vec![c1, c2, c3], child: gx }, locked: true, ..Default::default() }; g.hidden = false; v.push(g); let mut pic = shape(&mut p, "Pic", 0.0); pic.kind = ShapeKind::Picture { fill: PictureFill { media: img, ..Default::default() } }; pic.locked = true; v.push(pic); let mut cx = shape(&mut p, "Conn", 100.0); cx.kind = ShapeKind::Connector { start: None, end: None }; cx.geom = deckcraft_model::Geom::preset("straightConnector1"); cx.locked = true; v.push(cx); let mut tb = shape(&mut p, "Tbl", 200.0); tb.kind = ShapeKind::Table(Table::new(2, 2, 80.0, 30.0)); tb.locked = true; v.push(tb); if let Some(s) = p.slides.first_mut() { std::sync::Arc::make_mut(s).shapes = v; } p } fn find<'a>(s: &'a Slide, name: &str) -> &'a Shape { let mut found = None; deckcraft_model::walk(&s.shapes, &mut |x, _| { if found.is_none() && x.name == name { found = Some(x); } }); found.unwrap_or_else(|| panic!("no shape {name}")) } fn entries(bytes: &[u8]) -> Vec<(String, Vec)> { let mut z = zip::ZipArchive::new(Cursor::new(bytes)).expect("zip"); (0..z.len()) .map(|i| { let mut f = z.by_index(i).expect("entry"); let mut d = vec![]; f.read_to_end(&mut d).expect("read"); (f.name().to_string(), d) }) .collect() } fn rezip(es: &[(String, Vec)]) -> Vec { let mut z = zip::ZipWriter::new(Cursor::new(Vec::new())); let o = zip::write::SimpleFileOptions::default(); for (n, d) in es { z.start_file(n.as_str(), o).expect("start"); z.write_all(d).expect("write"); } z.finish().expect("finish").into_inner() } fn slide1_xml(bytes: &[u8]) -> String { let es = entries(bytes); let (_, d) = es.iter().find(|(n, _)| n == "ppt/slides/slide1.xml").expect("slide1"); String::from_utf8(d.clone()).expect("utf8") } /// The element following `name="…"` up to the end of the object's non-visual properties. fn nv_of<'a>(xml: &'a str, name: &str) -> &'a str { let i = xml.find(&format!("name=\"{name}\"")).unwrap_or_else(|| panic!("no {name}")); let rest = &xml[i..]; let j = rest.find(" = s.shapes.iter().map(|x| x.name.as_str()).collect(); assert_eq!(names, ["Locked", "Free", "Hidden text", "Group", "Pic", "Conn", "Tbl"]); let kids: Vec<&str> = find(s, "Group").children().iter().map(|x| x.name.as_str()).collect(); assert_eq!(kids, ["Child locked", "Child free", "Child hidden"]); } #[test] fn lock_writes_powerpoints_full_attribute_set() { let xml = slide1_xml(&deckcraft_pptx::export(&deck()).expect("export")); let sp = r#"noGrp="1" noRot="1" noMove="1" noResize="1" noEditPoints="1" noAdjustHandles="1" noChangeArrowheads="1" noChangeShapeType="1""#; assert!(nv_of(&xml, "Locked").contains(&format!("")), "{}", nv_of(&xml, "Locked")); assert!(nv_of(&xml, "Hidden text").contains(&format!(""))); assert!(nv_of(&xml, "Hidden text").contains(r#"hidden="1""#)); assert!(!nv_of(&xml, "Free").contains("Locks"), "an unlocked shape writes no locks: {}", nv_of(&xml, "Free")); assert!(nv_of(&xml, "Group").contains(r#""#)); assert!(nv_of(&xml, "Conn").contains(&format!("")), "{}", nv_of(&xml, "Conn")); assert!(nv_of(&xml, "Pic").contains(&format!(r#""#)), "{}", nv_of(&xml, "Pic")); assert!(nv_of(&xml, "Tbl").contains(r#""#)); } /// PowerPoint reads Lock only from the whole set: a lone `noMove`/`noResize` (another tool's /// partial lock), or the set without `noGrp`, isn't locked. #[test] fn partial_locks_are_not_lock() { let mut p = defaults::new_presentation(None); let a = shape(&mut p, "A", 0.0); let b = shape(&mut p, "B", 100.0); let c = shape(&mut p, "C", 200.0); let mut d = shape(&mut p, "D", 300.0); d.locked = true; if let Some(s) = p.slides.first_mut() { std::sync::Arc::make_mut(s).shapes = vec![a, b, c, d]; } let mut es = entries(&deckcraft_pptx::export(&p).expect("export")); let slide = es.iter_mut().find(|(n, _)| n == "ppt/slides/slide1.xml").expect("slide1"); let xml = String::from_utf8(slide.1.clone()).expect("utf8"); let swap = |xml: String, name: &str, locks: &str| { let i = xml.find(&format!("name=\"{name}\"")).expect("name"); let j = i + xml[i..].find("{locks}{}", &xml[..j], &xml[k..]) }; let xml = swap(xml, "A", r#""#); let xml = swap( xml, "B", r#""#, ); let xml = swap(xml, "C", r#""#); slide.1 = xml.into_bytes(); let q = deckcraft_pptx::import(&rezip(&es)).expect("import"); let s = &q.slides[0]; assert!(!find(s, "A").locked); assert!(!find(s, "B").locked); assert!(!find(s, "C").locked); assert!(find(s, "D").locked); } /// Unlocked objects written by PowerPoint keep an empty locks element; that's not locked. #[test] fn empty_locks_element_is_unlocked() { let mut p = defaults::new_presentation(None); let a = shape(&mut p, "A", 0.0); if let Some(s) = p.slides.first_mut() { std::sync::Arc::make_mut(s).shapes = vec![a]; } let mut es = entries(&deckcraft_pptx::export(&p).expect("export")); let slide = es.iter_mut().find(|(n, _)| n == "ppt/slides/slide1.xml").expect("slide1"); let xml = String::from_utf8(slide.1.clone()).expect("utf8").replacen("", "", 1); slide.1 = xml.into_bytes(); let q = deckcraft_pptx::import(&rezip(&es)).expect("import"); assert!(!find(&q.slides[0], "A").locked); }