Compare commits
1 Commits
main
...
fix/placeh
| Author | SHA1 | Date |
|---|---|---|
|
|
31f0d0c20d |
|
|
@ -14,7 +14,7 @@ use std::sync::Arc;
|
||||||
use deckcraft_geom::{Size, emu_to_pt};
|
use deckcraft_geom::{Size, emu_to_pt};
|
||||||
use deckcraft_model::text::TextBody;
|
use deckcraft_model::text::TextBody;
|
||||||
use deckcraft_model::{
|
use deckcraft_model::{
|
||||||
Background, Comment, CustomShow, HeaderFooter, Layout, LayoutId, LayoutType, Master, MasterId, MediaId, MediaItem, PhType, Presentation,
|
Background, Comment, CustomShow, Geom, HeaderFooter, Layout, LayoutId, LayoutType, Master, MasterId, MediaId, MediaItem, PhType, Presentation,
|
||||||
Properties, Section, ShowSettings, Slide, SlideId, defaults,
|
Properties, Section, ShowSettings, Slide, SlideId, defaults,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -387,6 +387,7 @@ pub fn import(bytes: &[u8]) -> Result<Presentation, PptxError> {
|
||||||
p.props.company = d.root.child("Company").map(|c| c.text()).unwrap_or_default();
|
p.props.company = d.root.child("Company").map(|c| c.text()).unwrap_or_default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inherit_placeholder_geometry(&mut p);
|
||||||
p.header_footer = header_footer(&p);
|
p.header_footer = header_footer(&p);
|
||||||
p.media = std::mem::take(&mut imp.media);
|
p.media = std::mem::take(&mut imp.media);
|
||||||
p.next_id = imp.next;
|
p.next_id = imp.next;
|
||||||
|
|
@ -419,6 +420,91 @@ pub fn color_map(cm: &El) -> Vec<(String, String)> {
|
||||||
cm.attrs.iter().filter(|(k, _)| !k.starts_with("xmlns")).map(|(k, v)| (k.clone(), v.clone())).collect()
|
cm.attrs.iter().filter(|(k, _)| !k.starts_with("xmlns")).map(|(k, v)| (k.clone(), v.clone())).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Placeholders read without a geometry (an empty custom geometry, see `shapes::sp`) take the
|
||||||
|
/// geometry of the layout or master placeholder they inherit from, as PowerPoint draws them: a
|
||||||
|
/// layout's picture placeholder cut into a shape keeps that shape on the slide. Anything left
|
||||||
|
/// is a rectangle.
|
||||||
|
fn inherit_placeholder_geometry(p: &mut Presentation) {
|
||||||
|
let unset = |g: &Geom| matches!(g, Geom::Custom { paths } if paths.is_empty());
|
||||||
|
// Masters first, then layouts (from their masters), then slides (from layouts and masters).
|
||||||
|
for m in &mut p.masters {
|
||||||
|
let m = Arc::make_mut(m);
|
||||||
|
walk_shapes_mut(&mut m.shapes, &mut |s| {
|
||||||
|
if unset(&s.geom) {
|
||||||
|
s.geom = Geom::default();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut layout_geoms: Vec<(LayoutId, usize, Geom)> = vec![];
|
||||||
|
for m in &p.masters {
|
||||||
|
for l in &m.layouts {
|
||||||
|
let ctx = deckcraft_model::resolve::Ctx::for_layout(p, m, l);
|
||||||
|
for (i, s) in l.shapes.iter().enumerate() {
|
||||||
|
if unset(&s.geom) {
|
||||||
|
let (_, mp) = deckcraft_model::resolve::parents(&ctx, s);
|
||||||
|
layout_geoms.push((l.id, i, mp.map(|m| m.geom.clone()).filter(|g| !unset(g)).unwrap_or_default()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for m in &mut p.masters {
|
||||||
|
let m = Arc::make_mut(m);
|
||||||
|
for l in &mut m.layouts {
|
||||||
|
for (id, i, g) in layout_geoms.iter().filter(|(id, _, _)| *id == l.id) {
|
||||||
|
let _ = id;
|
||||||
|
if let Some(s) = l.shapes.get_mut(*i) {
|
||||||
|
s.geom = g.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut slide_geoms: Vec<(usize, usize, Geom)> = vec![];
|
||||||
|
for (si, sl) in p.slides.iter().enumerate() {
|
||||||
|
let Some(ctx) = deckcraft_model::resolve::Ctx::for_slide(p, sl) else { continue };
|
||||||
|
for (i, s) in sl.shapes.iter().enumerate() {
|
||||||
|
if unset(&s.geom) {
|
||||||
|
let (lp, mp) = deckcraft_model::resolve::parents(&ctx, s);
|
||||||
|
let g = lp.map(|l| l.geom.clone()).filter(|g| !unset(g)).or_else(|| mp.map(|m| m.geom.clone()).filter(|g| !unset(g)));
|
||||||
|
slide_geoms.push((si, i, g.unwrap_or_default()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (si, i, g) in slide_geoms {
|
||||||
|
if let Some(sl) = p.slides.get_mut(si)
|
||||||
|
&& let Some(s) = Arc::make_mut(sl).shapes.get_mut(i)
|
||||||
|
{
|
||||||
|
s.geom = g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Placeholders inside groups (rare) and anything still unset: rectangles.
|
||||||
|
for sl in &mut p.slides {
|
||||||
|
walk_shapes_mut(&mut Arc::make_mut(sl).shapes, &mut |s| {
|
||||||
|
if unset(&s.geom) {
|
||||||
|
s.geom = Geom::default();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for m in &mut p.masters {
|
||||||
|
for l in &mut Arc::make_mut(m).layouts {
|
||||||
|
walk_shapes_mut(&mut l.shapes, &mut |s| {
|
||||||
|
if unset(&s.geom) {
|
||||||
|
s.geom = Geom::default();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every shape, groups included.
|
||||||
|
fn walk_shapes_mut(shapes: &mut [deckcraft_model::Shape], f: &mut dyn FnMut(&mut deckcraft_model::Shape)) {
|
||||||
|
for s in shapes {
|
||||||
|
f(s);
|
||||||
|
if let deckcraft_model::ShapeKind::Group { children, .. } = &mut s.kind {
|
||||||
|
walk_shapes_mut(children, f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn background(imp: &mut Imp, part: &Part, csld: &El) -> Option<Background> {
|
fn background(imp: &mut Imp, part: &Part, csld: &El) -> Option<Background> {
|
||||||
let bg = csld.child("bg")?;
|
let bg = csld.child("bg")?;
|
||||||
if let Some(pr) = bg.child("bgPr") {
|
if let Some(pr) = bg.child("bgPr") {
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,15 @@ fn preset_geom(pg: &El) -> Geom {
|
||||||
Geom::Preset { name, adj }
|
Geom::Preset { name, adj }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A placeholder whose `spPr` names no geometry takes its layout's (or master's) placeholder's:
|
||||||
|
/// marked here with an empty custom geometry, filled in once the layouts are read
|
||||||
|
/// (`inherit_placeholder_geometry`).
|
||||||
|
fn mark_inherited_geometry(s: &mut Shape, pr: Option<&El>) {
|
||||||
|
if s.ph.is_some() && pr.is_none_or(|pr| pr.child("prstGeom").is_none() && pr.child("custGeom").is_none()) {
|
||||||
|
s.geom = Geom::Custom { paths: vec![] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `spPr` → transform, geometry, fill, line, effects.
|
/// `spPr` → transform, geometry, fill, line, effects.
|
||||||
fn sp_pr(imp: &mut Imp, part: &Part, s: &mut Shape, pr: &El) {
|
fn sp_pr(imp: &mut Imp, part: &Part, s: &mut Shape, pr: &El) {
|
||||||
if let Some(x) = pr.child("xfrm") {
|
if let Some(x) = pr.child("xfrm") {
|
||||||
|
|
@ -229,6 +238,7 @@ fn sp(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El) -> Shape {
|
||||||
if let Some(pr) = e.child("spPr") {
|
if let Some(pr) = e.child("spPr") {
|
||||||
sp_pr(imp, part, &mut s, pr);
|
sp_pr(imp, part, &mut s, pr);
|
||||||
}
|
}
|
||||||
|
mark_inherited_geometry(&mut s, e.child("spPr"));
|
||||||
if e.bool("useBgFill") == Some(true) {
|
if e.bool("useBgFill") == Some(true) {
|
||||||
s.fill = Some(Fill::Background);
|
s.fill = Some(Fill::Background);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
//! Placeholder inheritance in decks written by PowerPoint.
|
||||||
|
|
||||||
|
use std::io::{Cursor, Read, Write};
|
||||||
|
|
||||||
|
use deckcraft_model::{Geom, defaults};
|
||||||
|
|
||||||
|
/// `bytes` with `edit` applied to every slide part.
|
||||||
|
fn edit_slides(bytes: &[u8], edit: impl Fn(String) -> String) -> Vec<u8> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_placeholder_without_geometry_takes_its_layouts() {
|
||||||
|
// PowerPoint leaves a slide placeholder's spPr empty and draws it with the layout
|
||||||
|
// placeholder's geometry (a picture placeholder cut into a shape keeps the shape).
|
||||||
|
let mut p = defaults::new_presentation(None);
|
||||||
|
let layout = p.slides[0].layout;
|
||||||
|
let m = std::sync::Arc::make_mut(&mut p.masters[0]);
|
||||||
|
let l = m.layouts.iter_mut().find(|l| l.id == layout).expect("layout");
|
||||||
|
for s in &mut l.shapes {
|
||||||
|
if s.ph.is_some() {
|
||||||
|
s.geom = Geom::Preset { name: "ellipse".into(), adj: vec![] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bytes = deckcraft_pptx::export(&p).expect("export");
|
||||||
|
// Strip the slide's own geometry, as PowerPoint writes inheriting placeholders.
|
||||||
|
let bare = edit_slides(&bytes, |s| {
|
||||||
|
let mut s = s;
|
||||||
|
while let Some(i) = s.find("<a:prstGeom") {
|
||||||
|
let Some(j) = s[i..].find("</a:prstGeom>").map(|j| j + i + "</a:prstGeom>".len()) else { break };
|
||||||
|
s.replace_range(i..j, "");
|
||||||
|
}
|
||||||
|
s
|
||||||
|
});
|
||||||
|
let q = deckcraft_pptx::import(&bare).expect("import");
|
||||||
|
let phs: Vec<_> = q.slides[0].shapes.iter().filter(|s| s.ph.is_some()).collect();
|
||||||
|
assert!(!phs.is_empty());
|
||||||
|
for s in phs {
|
||||||
|
assert!(matches!(&s.geom, Geom::Preset { name, .. } if name == "ellipse"), "{:?}", s.geom);
|
||||||
|
}
|
||||||
|
// A placeholder with its own geometry keeps it.
|
||||||
|
let first = p.slides[0].id;
|
||||||
|
for s in &mut p.slide_mut(first).expect("slide").shapes {
|
||||||
|
if s.ph.is_some() {
|
||||||
|
s.geom = Geom::Preset { name: "triangle".into(), adj: vec![] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let q = deckcraft_pptx::import(&deckcraft_pptx::export(&p).expect("export")).expect("import");
|
||||||
|
assert!(q.slides[0].shapes.iter().filter(|s| s.ph.is_some()).all(|s| matches!(&s.geom, Geom::Preset { name, .. } if name == "triangle")));
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue