diff --git a/crates/pptx/src/read/dml.rs b/crates/pptx/src/read/dml.rs index b4f08e0..01e08d7 100644 --- a/crates/pptx/src/read/dml.rs +++ b/crates/pptx/src/read/dml.rs @@ -77,6 +77,12 @@ pub fn fill_el(imp: &mut Imp, part: &Part, f: &El) -> Option { "solidFill" => Fill::Solid { color: color(f).unwrap_or(ColorRef::rgb(Rgba::BLACK)) }, "gradFill" => Fill::Gradient(gradient(f)), "blipFill" => match blip_fill(imp, part, f) { + // A picture filling a shape or background with neither `a:stretch` nor `a:tile` is + // tiled at its own size, as PowerPoint draws it. + Some(mut p) if f.child("stretch").is_none() && f.child("tile").is_none() => { + p.mode = PictureMode::Tile { tx: 0.0, ty: 0.0, sx: 1.0, sy: 1.0, flip: "none".into(), align: "tl".into() }; + Fill::Picture(p) + } Some(p) => Fill::Picture(p), None => Fill::None, }, diff --git a/crates/pptx/tests/fills.rs b/crates/pptx/tests/fills.rs new file mode 100644 index 0000000..cd096de --- /dev/null +++ b/crates/pptx/tests/fills.rs @@ -0,0 +1,81 @@ +//! 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()); + let id = ShapeId(p.alloc_id()); + 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("