//! Fills read from decks written by other tools. use std::io::{Cursor, Read, Write}; use deckcraft_geom::Xfrm; use deckcraft_model::style::{Fill, PictureFill, PictureMode}; use deckcraft_model::{Shape, ShapeId, defaults}; /// `bytes` with `edit` applied to every slide part. fn edit_slides(bytes: &[u8], edit: impl Fn(String) -> String) -> Vec { let mut z = zip::ZipArchive::new(Cursor::new(bytes)).expect("zip"); let mut out = zip::ZipWriter::new(Cursor::new(Vec::new())); let o = zip::write::SimpleFileOptions::default(); for i in 0..z.len() { let mut f = z.by_index(i).expect("entry"); let mut d = vec![]; f.read_to_end(&mut d).expect("read"); let name = f.name().to_string(); if name.starts_with("ppt/slides/slide") && name.ends_with(".xml") { d = edit(String::from_utf8(d).expect("utf8")).into_bytes(); } out.start_file(name, o).expect("start"); out.write_all(&d).expect("write"); } out.finish().expect("finish").into_inner() } fn png() -> Vec { let mut out = Cursor::new(vec![]); image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(4, 4, image::Rgb([10, 200, 30]))) .write_to(&mut out, image::ImageFormat::Png) .expect("png"); out.into_inner() } /// A deck with one shape filled with a stretched picture, exported. fn deck() -> Vec { let mut p = defaults::new_presentation(None); let media = p.add_media("tile.png", "image/png", png()).expect("media"); let id = ShapeId(p.alloc_id().expect("ids")); let first = p.slides[0].id; p.slide_mut(first).expect("slide").shapes.push(Shape { id, name: "Textured".into(), xfrm: Some(Xfrm::new(10.0, 10.0, 200.0, 100.0)), fill: Some(Fill::Picture(PictureFill { media, ..Default::default() })), ..Default::default() }); deckcraft_pptx::export(&p).expect("export") } fn textured_mode(bytes: &[u8]) -> PictureMode { let q = deckcraft_pptx::import(bytes).expect("import"); let s = q.slides[0].shapes.iter().find(|s| s.name == "Textured").expect("shape"); match &s.fill { Some(Fill::Picture(pf)) => pf.mode.clone(), other => panic!("{other:?}"), } } #[test] fn a_shape_picture_fill_without_stretch_or_tile_tiles() { // tdf#128596: `` on a shape, as some tools write it; // PowerPoint tiles the picture at its own size. let bare = edit_slides(&deck(), |s| { let s = regex_free_strip(&s, "", ""); assert!(!s.contains(" String { let Some(b) = s.find("", "")); let q = deckcraft_pptx::import(&edited).expect("import"); let s = q.slides[0].shapes.iter().find(|s| s.name == "Corner").expect("shape"); assert!(matches!(&s.fill, Some(Fill::Gradient(g)) if g.tile == [-1.0, -1.0, 0.0, 0.0]), "{:?}", s.fill); } #[test] fn a_table_naming_no_style_is_drawn_with_the_table_grid() { let mut p = defaults::new_presentation(None); let id = ShapeId(p.alloc_id().expect("ids")); let first = p.slides[0].id; let mut t = deckcraft_model::Table::new(2, 2, 200.0, 40.0); t.style = "none".into(); p.slide_mut(first).expect("slide").shapes.push(Shape { id, name: "Grid".into(), xfrm: Some(Xfrm::new(10.0, 10.0, 200.0, 80.0)), kind: deckcraft_model::ShapeKind::Table(t), ..Default::default() }); let bytes = deckcraft_pptx::export(&p).expect("export"); let style = |bytes: &[u8]| { let q = deckcraft_pptx::import(bytes).expect("import"); let s = q.slides[0].shapes.iter().find(|s| s.name == "Grid").expect("shape").clone(); match s.kind { deckcraft_model::ShapeKind::Table(t) => t.style, other => panic!("{other:?}"), } }; // Written with its style, it reads back as written. assert_eq!(style(&bytes), "none"); // Without a:tableStyleId PowerPoint draws thin dark grid lines. let bare = edit_slides(&bytes, |x| { let (Some(a), Some(b)) = (x.find(""), x.find("")) else { return x }; format!("{}{}", &x[..a], &x[b + "".len()..]) }); assert_eq!(style(&bare), "grid"); } #[test] fn black_and_white_and_inverted_alpha_pictures_round_trip() { let mut p = defaults::new_presentation(None); let media = p.add_media("tile.png", "image/png", png()).expect("media"); let id = ShapeId(p.alloc_id().expect("ids")); let first = p.slides[0].id; let mut pf = PictureFill { media, ..Default::default() }; pf.adjust.bilevel = Some(0.3); pf.adjust.alpha_inv = true; p.slide_mut(first).expect("slide").shapes.push(Shape { id, name: "BW".into(), xfrm: Some(Xfrm::new(10.0, 10.0, 200.0, 100.0)), fill: Some(Fill::Picture(pf)), ..Default::default() }); let bytes = deckcraft_pptx::export(&p).expect("export"); let q = deckcraft_pptx::import(&bytes).expect("import"); let s = q.slides[0].shapes.iter().find(|s| s.name == "BW").expect("shape"); match &s.fill { Some(Fill::Picture(pf)) => assert_eq!((pf.adjust.bilevel, pf.adjust.alpha_inv), (Some(0.3), true)), other => panic!("{other:?}"), } } #[test] fn an_upright_picture_fill_round_trips() { let bytes = deck(); let edited = edit_slides(&bytes, |x| x.replacen("rotWithShape=\"1\"", "rotWithShape=\"0\"", 1)); let q = deckcraft_pptx::import(&edited).expect("import"); let s = q.slides[0].shapes.iter().find(|s| s.name == "Textured").expect("shape"); let Some(Fill::Picture(pf)) = &s.fill else { panic!("{:?}", s.fill) }; assert!(pf.upright); let again = deckcraft_pptx::export(&q).expect("export"); let r = deckcraft_pptx::import(&again).expect("import"); let s = r.slides[0].shapes.iter().find(|s| s.name == "Textured").expect("shape"); assert!(matches!(&s.fill, Some(Fill::Picture(pf)) if pf.upright)); // Rotating with the shape is the default. assert!(matches!(textured_fill(&bytes), Some(Fill::Picture(pf)) if !pf.upright)); } fn textured_fill(bytes: &[u8]) -> Option { let q = deckcraft_pptx::import(bytes).expect("import"); q.slides[0].shapes.iter().find(|s| s.name == "Textured").and_then(|s| s.fill.clone()) }