mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-31 14:07:06 -06:00
Merge PR #438 integrated on land-r9: an exhausted id space refuses growth instead of duplicating ids (#407)
# Conflicts: # crates/engine/src/tests.rs
This commit is contained in:
@@ -189,7 +189,7 @@ fn group(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let xs: Vec<Xfrm> = members.iter().filter_map(|m| m.xfrm).collect();
|
||||
let b = union_bounds(&xs).ok_or_else(|| bad("arrange.group", "nothing to group"))?;
|
||||
let gx = Xfrm::new(b.x0, b.y0, b.width(), b.height());
|
||||
let id = ShapeId(doc.alloc_id());
|
||||
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("arrange.group", NO_ID_SPACE))?);
|
||||
let n = super::current_shapes(doc, sel).map(|v| v.len()).unwrap_or(0);
|
||||
let g = Shape {
|
||||
id,
|
||||
|
||||
@@ -173,7 +173,10 @@ fn artistic_effect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit(move |doc, sel| {
|
||||
let mut set: Vec<(ShapeId, MediaId, Option<MediaId>)> = Vec::new();
|
||||
for (id, original, processed) in &results {
|
||||
let m = processed.as_ref().map(|(name, png)| doc.add_media(name, "image/png", png.clone()));
|
||||
let m = match processed {
|
||||
Some((name, png)) => Some(doc.add_media(name, "image/png", png.clone()).ok_or_else(|| bad("picture.artisticEffect", NO_ID_SPACE))?),
|
||||
None => None,
|
||||
};
|
||||
set.push((*id, *original, m));
|
||||
}
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("picture.artisticEffect", "no slide"))?;
|
||||
|
||||
@@ -165,8 +165,10 @@ fn remove_background(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let n = done.len();
|
||||
s.edit(|doc, sel| {
|
||||
let added: Vec<(ShapeId, deckcraft_model::MediaId)> =
|
||||
done.into_iter().map(|(id, name, png)| (id, doc.add_media(&name, "image/png", png))).collect();
|
||||
let added: Vec<(ShapeId, deckcraft_model::MediaId)> = done
|
||||
.into_iter()
|
||||
.map(|(id, name, png)| Ok((id, doc.add_media(&name, "image/png", png).ok_or_else(|| bad("picture.removeBackground", NO_ID_SPACE))?)))
|
||||
.collect::<Result<_>>()?;
|
||||
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad("picture.removeBackground", "no slide"))?;
|
||||
for (id, media) in added {
|
||||
if let Some(sh) = deckcraft_model::find_shape_mut(shapes, id)
|
||||
|
||||
@@ -169,14 +169,19 @@ fn compress(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let old = m.data.len();
|
||||
pack(&m.data, crop, w, h, ppi, delete_cropped).map(|pk| (pk, old, m.name.clone()))
|
||||
});
|
||||
let v = packed.map(|(pk, old, name)| {
|
||||
before += old;
|
||||
after += pk.bytes.len();
|
||||
let stem = std::path::Path::new(&name).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or(name);
|
||||
let ext = if pk.content_type == "image/jpeg" { "jpg" } else { "png" };
|
||||
let id = doc.add_media(&format!("{stem}.{ext}"), pk.content_type, pk.bytes);
|
||||
(id, pk.crop, old)
|
||||
});
|
||||
let v = match packed {
|
||||
Some((pk, old, name)) => {
|
||||
before += old;
|
||||
after += pk.bytes.len();
|
||||
let stem = std::path::Path::new(&name).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or(name);
|
||||
let ext = if pk.content_type == "image/jpeg" { "jpg" } else { "png" };
|
||||
let id = doc
|
||||
.add_media(&format!("{stem}.{ext}"), pk.content_type, pk.bytes)
|
||||
.ok_or_else(|| bad("picture.compress", NO_ID_SPACE))?;
|
||||
Some((id, pk.crop, old))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
cache.insert(key, v);
|
||||
v
|
||||
}
|
||||
@@ -291,8 +296,8 @@ mod tests {
|
||||
let big = png(1200, 600);
|
||||
let processed = png(1100, 600);
|
||||
s.edit(|doc, _| {
|
||||
let original = doc.add_media("original.png", "image/png", big.clone());
|
||||
let shown = doc.add_media("processed.png", "image/png", processed.clone());
|
||||
let original = doc.add_media("original.png", "image/png", big.clone()).expect("media");
|
||||
let shown = doc.add_media("processed.png", "image/png", processed.clone()).expect("media");
|
||||
let slide = Arc::make_mut(doc.slides.first_mut().ok_or_else(|| bad("test", "no slide"))?);
|
||||
let xfrm = Some(deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 36.0));
|
||||
let with_effect =
|
||||
|
||||
@@ -166,7 +166,7 @@ fn browse_theme(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// The theme's pictures (format-scheme blip fills) get ids of this deck.
|
||||
let mut map = std::collections::HashMap::new();
|
||||
for m in &media {
|
||||
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone());
|
||||
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone()).ok_or_else(|| bad("design.browseTheme", NO_ID_SPACE))?;
|
||||
map.insert(m.id.0, id.0);
|
||||
}
|
||||
let theme = if map.is_empty() { theme.clone() } else { remap_media_ids(&theme, &map)? };
|
||||
@@ -519,7 +519,7 @@ fn background(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
} else if p.get("picture").is_some() {
|
||||
let (name, bytes) = super::insert::media_bytes(&with_param(p, "data", p.get("picture").cloned().unwrap_or_default()), "design.background")?;
|
||||
let ct = super::insert::content_type(&name, &bytes);
|
||||
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
|
||||
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("design.background", NO_ID_SPACE)))?;
|
||||
let mut pf = deckcraft_model::style::PictureFill { media, ..Default::default() };
|
||||
if let Some(pp) = &props {
|
||||
apply_picture_props(&mut pf, pp);
|
||||
@@ -853,7 +853,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let mut new_ids = vec![];
|
||||
for _ in 0..3 {
|
||||
new_ids.push(ShapeId(doc.alloc_id()));
|
||||
new_ids.push(ShapeId(doc.alloc_id().ok_or_else(|| bad("design.headerFooter", NO_ID_SPACE))?));
|
||||
}
|
||||
let Some(sl) = doc.slides.get_mut(i) else { continue };
|
||||
let sl = Arc::make_mut(sl);
|
||||
@@ -1024,7 +1024,7 @@ fn handout_view(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
};
|
||||
let size = doc.notes_size;
|
||||
for (_, k) in HANDOUT_KINDS {
|
||||
let id = ShapeId(doc.alloc_id());
|
||||
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("view.handoutMaster", NO_ID_SPACE))?);
|
||||
m.shapes.push(handout_placeholder(k, size, id));
|
||||
}
|
||||
doc.handout_master = Some(Arc::new(m));
|
||||
@@ -1044,7 +1044,11 @@ fn handout_placeholders(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let size = doc.notes_size;
|
||||
let mut ids = vec![];
|
||||
for (key, _) in HANDOUT_KINDS {
|
||||
ids.push(p.get(key).and_then(Value::as_bool).map(|_| ShapeId(doc.alloc_id())));
|
||||
let id = match p.get(key).and_then(Value::as_bool) {
|
||||
Some(_) => Some(ShapeId(doc.alloc_id().ok_or_else(|| bad("handout.placeholders", NO_ID_SPACE))?)),
|
||||
None => None,
|
||||
};
|
||||
ids.push(id);
|
||||
}
|
||||
let m = Arc::make_mut(doc.handout_master.as_mut().ok_or_else(|| bad("handout.placeholders", "no handout master"))?);
|
||||
for ((key, kind), id) in HANDOUT_KINDS.into_iter().zip(ids) {
|
||||
@@ -1128,7 +1132,7 @@ fn notes_view(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
});
|
||||
let size = doc.notes_size;
|
||||
for (_, k) in NOTES_KINDS {
|
||||
let id = ShapeId(doc.alloc_id());
|
||||
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("view.notesMaster", NO_ID_SPACE))?);
|
||||
m.shapes.push(notes_placeholder(k, size, id));
|
||||
}
|
||||
doc.notes_master = Some(Arc::new(m));
|
||||
@@ -1146,7 +1150,8 @@ fn notes_view(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
fn notes_placeholders(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit(|doc, _| {
|
||||
let size = doc.notes_size;
|
||||
let ids: Vec<ShapeId> = NOTES_KINDS.iter().map(|_| ShapeId(doc.alloc_id())).collect();
|
||||
let ids: Vec<ShapeId> =
|
||||
NOTES_KINDS.iter().map(|_| doc.alloc_id().map(ShapeId).ok_or_else(|| bad("notes.placeholders", NO_ID_SPACE))).collect::<Result<_>>()?;
|
||||
let m = Arc::make_mut(doc.notes_master.as_mut().ok_or_else(|| bad("notes.placeholders", "no notes master"))?);
|
||||
for ((key, kind), id) in NOTES_KINDS.into_iter().zip(ids) {
|
||||
let Some(on) = p.get(key).and_then(Value::as_bool) else { continue };
|
||||
@@ -1180,8 +1185,8 @@ fn insert_layout(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").unwrap_or("Custom Layout").to_string();
|
||||
s.edit(|doc, sel| {
|
||||
let mi = master_index(doc, sel);
|
||||
let id = deckcraft_model::LayoutId(doc.alloc_id());
|
||||
let tid = ShapeId(doc.alloc_id());
|
||||
let id = deckcraft_model::LayoutId(doc.alloc_id().ok_or_else(|| bad("master.insertLayout", NO_ID_SPACE))?);
|
||||
let tid = ShapeId(doc.alloc_id().ok_or_else(|| bad("master.insertLayout", NO_ID_SPACE))?);
|
||||
let size = doc.slide_size;
|
||||
let m = Arc::make_mut(doc.masters.get_mut(mi).ok_or_else(|| bad("master.insertLayout", "no master"))?);
|
||||
let title = m.shapes.iter().find(|x| x.ph_type() == Some(PhType::Title)).cloned().map(|mut t| {
|
||||
@@ -1273,7 +1278,7 @@ fn insert_placeholder(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
text: Some(TextBody::default()),
|
||||
..Default::default()
|
||||
};
|
||||
let id = super::insert::add_shape(s, shape, true)?;
|
||||
let id = super::insert::add_shape(s, "master.insertPlaceholder", shape, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
@@ -1342,7 +1347,7 @@ fn placeholders(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit(|doc, sel| {
|
||||
let mut ids = Vec::with_capacity(add.len());
|
||||
for _ in 0..add.len() {
|
||||
ids.push(ShapeId(doc.alloc_id()));
|
||||
ids.push(ShapeId(doc.alloc_id().ok_or_else(|| bad("master.placeholders", NO_ID_SPACE))?));
|
||||
}
|
||||
let prototypes = match sel.target {
|
||||
Target::Master { master } | Target::Layout { master, .. } => doc.masters.get(master).map(|m| m.shapes.clone()).unwrap_or_default(),
|
||||
|
||||
@@ -184,22 +184,29 @@ fn cut(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// attached to a shape that was copied too follow its copy; ends attached to a shape outside the
|
||||
/// copied set are detached (the connector keeps its geometry), so a copy never stays glued to
|
||||
/// the original or to an unrelated shape that happens to share an id in another deck.
|
||||
pub(crate) fn reid(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]) -> HashMap<ShapeId, ShapeId> {
|
||||
fn rec(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape], map: &mut HashMap<ShapeId, ShapeId>, depth: usize) {
|
||||
pub(crate) fn reid(cmd: &str, doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]) -> Result<HashMap<ShapeId, ShapeId>> {
|
||||
fn rec(
|
||||
cmd: &str,
|
||||
doc: &mut deckcraft_model::Presentation,
|
||||
shapes: &mut [Shape],
|
||||
map: &mut HashMap<ShapeId, ShapeId>,
|
||||
depth: usize,
|
||||
) -> Result<()> {
|
||||
if depth > 64 {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
for s in shapes {
|
||||
let id = ShapeId(doc.alloc_id());
|
||||
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?);
|
||||
map.insert(s.id, id);
|
||||
s.id = id;
|
||||
if let Some(ch) = s.children_mut() {
|
||||
rec(doc, ch, map, depth + 1);
|
||||
rec(cmd, doc, ch, map, depth + 1)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
let mut map = HashMap::new();
|
||||
rec(doc, shapes, &mut map, 0);
|
||||
rec(cmd, doc, shapes, &mut map, 0)?;
|
||||
walk_mut(shapes, &mut |s| {
|
||||
if let deckcraft_model::ShapeKind::Connector { start, end } = &mut s.kind {
|
||||
for e in [start, end] {
|
||||
@@ -207,7 +214,7 @@ pub(crate) fn reid(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]
|
||||
}
|
||||
}
|
||||
});
|
||||
map
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// Point copied animations at the copies: targets and triggers in `map` follow it; an animation
|
||||
@@ -267,19 +274,19 @@ fn remap_media(shapes: &mut [Shape], map: &HashMap<MediaId, MediaId>) {
|
||||
|
||||
/// Add the clipboard's media to `doc` and return the ids that changed: an id already used in
|
||||
/// `doc` by other bytes (another deck) gets a fresh id, and the pasted shapes must use it.
|
||||
fn bring_media(doc: &mut deckcraft_model::Presentation, media: &[deckcraft_model::MediaItem]) -> HashMap<MediaId, MediaId> {
|
||||
fn bring_media(cmd: &str, doc: &mut deckcraft_model::Presentation, media: &[deckcraft_model::MediaItem]) -> Result<HashMap<MediaId, MediaId>> {
|
||||
let mut map = HashMap::new();
|
||||
for m in media {
|
||||
match doc.media(m.id) {
|
||||
None => doc.media.push(m.clone()),
|
||||
Some(x) if x.data == m.data => {}
|
||||
Some(_) => {
|
||||
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone());
|
||||
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone()).ok_or_else(|| bad(cmd, NO_ID_SPACE))?;
|
||||
map.insert(m.id, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn paste(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -308,12 +315,12 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !clip.slides.is_empty() {
|
||||
let at = s.doc()?.selection.slide + 1;
|
||||
return s.edit(|doc, sel| {
|
||||
let media = bring_media(doc, &clip.media);
|
||||
let media = bring_media("edit.paste", doc, &clip.media)?;
|
||||
let mut new_ids = vec![];
|
||||
for (k, sl) in clip.slides.iter().enumerate() {
|
||||
let mut sl = sl.clone();
|
||||
sl.id = SlideId(doc.alloc_id());
|
||||
let map = reid(doc, &mut sl.shapes);
|
||||
sl.id = SlideId(doc.alloc_id().ok_or_else(|| bad("edit.paste", NO_ID_SPACE))?);
|
||||
let map = reid("edit.paste", doc, &mut sl.shapes)?;
|
||||
remap_animations(&mut sl.animations, &map);
|
||||
remap_media(&mut sl.shapes, &media);
|
||||
if let Some(deckcraft_model::Background::Fill { fill: deckcraft_model::Fill::Picture(pf) }) = &mut sl.background
|
||||
@@ -341,9 +348,9 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let same_slide = s.doc().ok().is_some_and(|d| clip.shapes.iter().any(|c| d.shape(c.id).is_some()));
|
||||
let off = if same_slide { off } else { 0.0 };
|
||||
return s.edit(|doc, sel| {
|
||||
let media = bring_media(doc, &clip.media);
|
||||
let media = bring_media("edit.paste", doc, &clip.media)?;
|
||||
let mut shapes = clip.shapes.clone();
|
||||
reid(doc, &mut shapes);
|
||||
reid("edit.paste", doc, &mut shapes)?;
|
||||
remap_media(&mut shapes, &media);
|
||||
for sh in &mut shapes {
|
||||
if let Some(x) = sh.xfrm.as_mut() {
|
||||
@@ -491,7 +498,7 @@ fn paste_picture(s: &mut Session) -> Result<Value> {
|
||||
}
|
||||
let st = s.doc()?;
|
||||
let mut doc = (*st.doc).clone();
|
||||
bring_media(&mut doc, &clip.media);
|
||||
bring_media("edit.pasteSpecial", &mut doc, &clip.media)?;
|
||||
let mut slide = st.current_slide().cloned().ok_or_else(|| bad("edit.pasteSpecial", "no slide"))?;
|
||||
let xs: Vec<deckcraft_geom::Xfrm> = clip.shapes.iter().filter_map(|x| x.xfrm).collect();
|
||||
let b = super::arrange::union_bounds(&xs).ok_or_else(|| bad("edit.pasteSpecial", "nothing to draw"))?;
|
||||
@@ -527,7 +534,7 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let list = super::current_shapes(doc, sel).cloned().unwrap_or_default();
|
||||
let mut copies: Vec<Shape> = list.iter().filter(|x| ids.contains(&x.id)).cloned().collect();
|
||||
let boxes: Vec<_> = copies.iter().map(|c| xfrm_of(doc, sel, c)).collect();
|
||||
reid(doc, &mut copies);
|
||||
reid("edit.duplicate", doc, &mut copies)?;
|
||||
for (c, b) in copies.iter_mut().zip(boxes) {
|
||||
let mut x = b;
|
||||
x.x += off;
|
||||
|
||||
@@ -149,9 +149,9 @@ fn default_rect(s: &Session, w: f64, h: f64) -> Xfrm {
|
||||
Xfrm::new((size.width - w) / 2.0, (size.height - h) / 2.0, w, h)
|
||||
}
|
||||
|
||||
pub(crate) fn add_shape(s: &mut Session, mut shape: Shape, select: bool) -> Result<ShapeId> {
|
||||
pub(crate) fn add_shape(s: &mut Session, cmd: &str, mut shape: Shape, select: bool) -> Result<ShapeId> {
|
||||
s.edit(|doc, sel| {
|
||||
shape.id = ShapeId(doc.alloc_id());
|
||||
shape.id = ShapeId(doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?);
|
||||
if shape.name.is_empty() {
|
||||
let base = match &shape.kind {
|
||||
ShapeKind::Picture { .. } => "Picture".to_string(),
|
||||
@@ -222,7 +222,7 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let (Some(look), false) = (&s.default_look, line_like) {
|
||||
super::format::apply_painted(&mut shape, look);
|
||||
}
|
||||
let id = add_shape(s, shape, true)?;
|
||||
let id = add_shape(s, "shape.insert", shape, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ fn insert_icon(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
style: Some(ShapeStyle::line(SchemeSlot::Tx1)),
|
||||
..Default::default()
|
||||
};
|
||||
let id = add_shape(s, shape, true)?;
|
||||
let id = add_shape(s, "insert.icon", shape, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ fn text_box(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
t.body.wrap = Some(false);
|
||||
}
|
||||
}
|
||||
let id = add_shape(s, shape, true)?;
|
||||
let id = add_shape(s, "insert.textBox", shape, true)?;
|
||||
super::text::fit_text_box(s, id)?;
|
||||
if text.is_empty() {
|
||||
// Start typing immediately.
|
||||
@@ -426,7 +426,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
_ => [0.0; 4],
|
||||
};
|
||||
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
|
||||
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("insert.picture", NO_ID_SPACE)))?;
|
||||
let shape = Shape {
|
||||
xfrm: Some(rect),
|
||||
kind: ShapeKind::Picture { fill: PictureFill { media, crop, ..Default::default() } },
|
||||
@@ -453,7 +453,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})?;
|
||||
return Ok(json!({"id": ph_id}));
|
||||
}
|
||||
let id = add_shape(s, shape, true)?;
|
||||
let id = add_shape(s, "insert.picture", shape, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
@@ -544,7 +544,10 @@ fn commit_album(s: &mut Session, items: Vec<AlbumItem>, opts: AlbumOpts, replace
|
||||
let result = (|| {
|
||||
let album_id = match replace.as_ref().map(|(id, _)| *id) {
|
||||
Some(id) if id != 0 => id,
|
||||
_ => s.edit(|doc, _| Ok(doc.alloc_id()))?,
|
||||
_ => {
|
||||
let cmd = if replace.is_some() { "insert.photoAlbumEdit" } else { "insert.photoAlbum" };
|
||||
s.edit(|doc, _| doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE)))?
|
||||
}
|
||||
};
|
||||
let at = if let Some((_, ids)) = &replace {
|
||||
let st = s.doc()?;
|
||||
@@ -1190,7 +1193,7 @@ fn table(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Table(t), ..Default::default() }, true)?;
|
||||
let id = add_shape(s, "insert.table", Shape { xfrm: Some(rect), kind: ShapeKind::Table(t), ..Default::default() }, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
@@ -1247,7 +1250,7 @@ fn chart(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let size = s.doc()?.doc.slide_size;
|
||||
let rect = rect_param(p, "rect").unwrap_or(Xfrm::new(size.width * 0.15, size.height * 0.15, size.width * 0.7, size.height * 0.7));
|
||||
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Chart(Box::new(c)), ..Default::default() }, true)?;
|
||||
let id = add_shape(s, "insert.chart", Shape { xfrm: Some(rect), kind: ShapeKind::Chart(Box::new(c)), ..Default::default() }, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
@@ -1299,12 +1302,15 @@ fn media(s: &mut Session, p: &Value, video: bool) -> Result<Value> {
|
||||
});
|
||||
let data = std::sync::Arc::try_unwrap(bytes).unwrap_or_else(|a| (*a).clone());
|
||||
let (media, poster) = s.edit(|doc, _| {
|
||||
let media = doc.add_media(&name, ct, data);
|
||||
let poster = poster_png.map(|png| doc.add_media(&format!("{name}-poster.png"), "image/png", png));
|
||||
let media = doc.add_media(&name, ct, data).ok_or_else(|| bad("insert.media", NO_ID_SPACE))?;
|
||||
let poster = match poster_png {
|
||||
Some(png) => Some(doc.add_media(&format!("{name}-poster.png"), "image/png", png).ok_or_else(|| bad("insert.media", NO_ID_SPACE))?),
|
||||
None => None,
|
||||
};
|
||||
Ok((media, poster))
|
||||
})?;
|
||||
let clip = MediaClip { media, video, poster, volume: 1.0, duration_ms, width, height, ..Default::default() };
|
||||
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Media(clip), descr: name, ..Default::default() }, true)?;
|
||||
let id = add_shape(s, "insert.media", Shape { xfrm: Some(rect), kind: ShapeKind::Media(clip), descr: name, ..Default::default() }, true)?;
|
||||
if let Some(w) = &warning {
|
||||
s.ui_requests.push(crate::UiRequest::Message { text: w.clone() });
|
||||
}
|
||||
@@ -1346,7 +1352,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = add_shape(s, shape, true)?;
|
||||
let id = add_shape(s, "insert.wordArt", shape, true)?;
|
||||
super::text::fit_text_box(s, id)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
@@ -1676,7 +1682,7 @@ fn action_button(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let id = add_shape(s, shape, true)?;
|
||||
let id = add_shape(s, "insert.actionButton", shape, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ fn poster_frame(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let data = s.doc()?.doc.media(m.media).map(|i| i.data.clone()).ok_or_else(|| EngineError::Other("the media data is missing".into()))?;
|
||||
let png = poster_png(&data, ms).ok_or_else(|| EngineError::Other("no frame could be decoded there".into()))?;
|
||||
let name = format!("poster{id}.png");
|
||||
let poster = s.edit(|doc, _| Ok(doc.add_media(&name, "image/png", png)))?;
|
||||
let poster = s.edit(|doc, _| doc.add_media(&name, "image/png", png).ok_or_else(|| bad("media.posterFrame", NO_ID_SPACE)))?;
|
||||
edit_shapes(s, &json!({"id": id}), "media.posterFrame", |sh| {
|
||||
if let ShapeKind::Media(m) = &mut sh.kind {
|
||||
m.poster = Some(poster);
|
||||
|
||||
@@ -237,7 +237,7 @@ fn merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
made.push(sh);
|
||||
}
|
||||
for sh in &mut made {
|
||||
sh.id = ShapeId(doc.alloc_id());
|
||||
sh.id = ShapeId(doc.alloc_id().ok_or_else(|| bad("shape.merge", NO_ID_SPACE))?);
|
||||
new_ids.push(sh.id);
|
||||
}
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.merge", "no slide"))?;
|
||||
|
||||
@@ -293,6 +293,10 @@ pub fn parse_params(doc: &str) -> std::result::Result<Vec<Param<'_>>, String> {
|
||||
|
||||
// ---------- param helpers ----------
|
||||
|
||||
/// The deck has used every object id (a hostile file can arrive that way): it cannot grow, and
|
||||
/// growing would duplicate ids (#407).
|
||||
pub(crate) const NO_ID_SPACE: &str = "the presentation has used every object id and can hold no more";
|
||||
|
||||
pub(crate) fn bad(cmd: &str, msg: impl Into<String>) -> EngineError {
|
||||
EngineError::BadParams { cmd: cmd.into(), msg: msg.into() }
|
||||
}
|
||||
|
||||
@@ -61,12 +61,12 @@ fn remap_connectors(shapes: &mut [deckcraft_model::Shape], map: &HashMap<ShapeId
|
||||
}
|
||||
|
||||
/// Fresh shape ids for `shapes`, and the old → new map.
|
||||
fn renumber(doc: &mut Presentation, shapes: &mut [deckcraft_model::Shape]) -> HashMap<ShapeId, ShapeId> {
|
||||
fn renumber(doc: &mut Presentation, shapes: &mut [deckcraft_model::Shape]) -> Result<HashMap<ShapeId, ShapeId>> {
|
||||
let old = shape_ids(shapes);
|
||||
super::edit::reid(doc, shapes);
|
||||
super::edit::reid("slide.reuse", doc, shapes)?;
|
||||
let map: HashMap<ShapeId, ShapeId> = old.into_iter().zip(shape_ids(shapes)).collect();
|
||||
remap_connectors(shapes, &map, 0);
|
||||
map
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// The destination layout a reused slide moves to: same name, else same kind, else the first.
|
||||
@@ -120,16 +120,16 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
masters_done.push(m.id);
|
||||
let mut used = vec![];
|
||||
media_refs::visit::master(m, &mut |id: &MediaId| used.push(*id));
|
||||
copy_media(doc, &src, &used, &mut media_map);
|
||||
copy_media(doc, &src, &used, &mut media_map)?;
|
||||
let mut m = m.clone();
|
||||
media_refs::visit_mut::master(&mut m, &mut remap(&media_map));
|
||||
m.id = MasterId(doc.alloc_id());
|
||||
renumber(doc, &mut m.shapes);
|
||||
m.id = MasterId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
|
||||
renumber(doc, &mut m.shapes)?;
|
||||
for l in &mut m.layouts {
|
||||
let new = LayoutId(doc.alloc_id());
|
||||
let new = LayoutId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
|
||||
layout_map.insert(l.id, new);
|
||||
l.id = new;
|
||||
renumber(doc, &mut l.shapes);
|
||||
renumber(doc, &mut l.shapes)?;
|
||||
}
|
||||
doc.masters.push(Arc::new(m));
|
||||
}
|
||||
@@ -137,11 +137,11 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut new_ids = vec![];
|
||||
for (k, &i) in picked.iter().enumerate() {
|
||||
let Some(from) = src.slides.get(i) else { continue };
|
||||
copy_media(doc, &src, &media_refs::slide_media(from), &mut media_map);
|
||||
copy_media(doc, &src, &media_refs::slide_media(from), &mut media_map)?;
|
||||
let mut sl: Slide = from.as_ref().clone();
|
||||
media_refs::visit_mut::slide(&mut sl, &mut remap(&media_map));
|
||||
sl.id = SlideId(doc.alloc_id());
|
||||
let map = renumber(doc, &mut sl.shapes);
|
||||
sl.id = SlideId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
|
||||
let map = renumber(doc, &mut sl.shapes)?;
|
||||
for a in &mut sl.animations {
|
||||
a.shape = map.get(&a.shape).copied().unwrap_or(a.shape);
|
||||
a.trigger = a.trigger.map(|t| map.get(&t).copied().unwrap_or(t));
|
||||
@@ -167,7 +167,7 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// Copy the source media `used` into `doc` under fresh ids, once each (identical bytes already in
|
||||
/// `doc` are shared; a linked item keeps its link).
|
||||
fn copy_media(doc: &mut Presentation, src: &Presentation, used: &[MediaId], map: &mut HashMap<MediaId, MediaId>) {
|
||||
fn copy_media(doc: &mut Presentation, src: &Presentation, used: &[MediaId], map: &mut HashMap<MediaId, MediaId>) -> Result<()> {
|
||||
for &id in used {
|
||||
if map.contains_key(&id) {
|
||||
continue;
|
||||
@@ -175,15 +175,16 @@ fn copy_media(doc: &mut Presentation, src: &Presentation, used: &[MediaId], map:
|
||||
let Some(m) = src.media(id) else { continue };
|
||||
let new = if m.link.is_some() {
|
||||
let mut item = m.clone();
|
||||
item.id = MediaId(doc.alloc_id());
|
||||
item.id = MediaId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
|
||||
let new = item.id;
|
||||
doc.media.push(item);
|
||||
new
|
||||
} else {
|
||||
doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone())
|
||||
doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone()).ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?
|
||||
};
|
||||
map.insert(id, new);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -201,8 +202,11 @@ mod tests {
|
||||
fn reused_slides_bring_every_media_reference() {
|
||||
let mut src = defaults::new_presentation(None);
|
||||
let blobs: Vec<Vec<u8>> = (0..8u8).map(|i| vec![0xD0, i, i, i]).collect();
|
||||
let ids: Vec<MediaId> =
|
||||
blobs.iter().enumerate().map(|(i, b)| src.add_media(&format!("m{i}.bin"), "application/octet-stream", b.clone())).collect();
|
||||
let ids: Vec<MediaId> = blobs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, b)| src.add_media(&format!("m{i}.bin"), "application/octet-stream", b.clone()).expect("media"))
|
||||
.collect();
|
||||
let id = |i: usize| ids.get(i).copied().unwrap_or_default();
|
||||
let mut text = TextBody::from_text("bullet");
|
||||
if let Some(p) = text.paragraphs.first_mut() {
|
||||
@@ -246,7 +250,7 @@ mod tests {
|
||||
|
||||
let mut s = Session::with_new();
|
||||
let own = vec![1u8, 2, 3];
|
||||
let own_id = s.edit(|doc, _| Ok(doc.add_media("own.bin", "application/octet-stream", own.clone()))).expect("own media");
|
||||
let own_id = s.edit(|doc, _| Ok(doc.add_media("own.bin", "application/octet-stream", own.clone()).expect("media"))).expect("own media");
|
||||
s.execute("slide.reuse", &json!({"data": super::super::base64_encode(&bytes), "name": "src.deckcraft", "slides": [0]})).expect("reuse");
|
||||
let doc = &s.doc().expect("doc").doc;
|
||||
assert_eq!(doc.media(own_id).map(|m| m.data.as_slice() == own.as_slice()), Some(true), "own media untouched");
|
||||
|
||||
@@ -684,7 +684,7 @@ fn accept(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
changes.into_iter().nth(index.unwrap_or(0))
|
||||
};
|
||||
let Some(c) = c else { break };
|
||||
let Some(at) = crate::compare::accept(doc, &rev, &c) else { break };
|
||||
let Some(at) = crate::compare::accept(doc, &rev, &c)? else { break };
|
||||
accepted += 1;
|
||||
sel.slide = at;
|
||||
sel.shapes.clear();
|
||||
|
||||
@@ -538,7 +538,7 @@ fn fill(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
} else if p.get("picture").is_some() {
|
||||
let (name, bytes) = super::insert::media_bytes(&with_param(p, "data", p.get("picture").cloned().unwrap_or_default()), "shape.fill")?;
|
||||
let ct = super::insert::content_type(&name, &bytes);
|
||||
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
|
||||
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("shape.fill", NO_ID_SPACE)))?;
|
||||
Some(Some(Fill::Picture(PictureFill { media, ..Default::default() })))
|
||||
} else {
|
||||
None
|
||||
@@ -1096,7 +1096,7 @@ fn picture_reset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn picture_change(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (name, bytes) = super::insert::media_bytes(p, "picture.change")?;
|
||||
let ct = super::insert::content_type(&name, &bytes);
|
||||
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
|
||||
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("picture.change", NO_ID_SPACE)))?;
|
||||
edit_shapes(s, p, "picture.change", |sh| {
|
||||
if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
|
||||
fill.media = media;
|
||||
@@ -1454,7 +1454,7 @@ fn ink_to_shape(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
list.remove(pos);
|
||||
}
|
||||
for (preset, xfrm, color, width) in made {
|
||||
let id = ShapeId(doc.alloc_id());
|
||||
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("draw.inkToShape", NO_ID_SPACE))?);
|
||||
let name = format!(
|
||||
"{} {}",
|
||||
match preset {
|
||||
|
||||
@@ -115,7 +115,7 @@ fn new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut slide = defaults::new_slide(doc, layout);
|
||||
let mut slide = defaults::new_slide(doc, layout).ok_or_else(|| bad("slide.new", NO_ID_SPACE))?;
|
||||
if let Some(t) = str_param(p, "title")
|
||||
&& let Some(sh) = slide.shapes.iter_mut().find(|x| x.ph_type().is_some_and(|k| k.is_title()))
|
||||
{
|
||||
@@ -186,9 +186,9 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for (k, i) in indices.iter().enumerate() {
|
||||
let Some(src) = doc.slides.get(*i).map(|x| x.as_ref().clone()) else { continue };
|
||||
let mut copy = src;
|
||||
copy.id = SlideId(doc.alloc_id());
|
||||
copy.id = SlideId(doc.alloc_id().ok_or_else(|| bad("slide.duplicate", NO_ID_SPACE))?);
|
||||
// Animations follow the new shape ids, triggers included.
|
||||
let map = super::edit::reid(doc, &mut copy.shapes);
|
||||
let map = super::edit::reid("slide.duplicate", doc, &mut copy.shapes)?;
|
||||
super::edit::remap_animations(&mut copy.animations, &map);
|
||||
let section = doc.section_of(src_id(doc, *i));
|
||||
let at = (last + 1 + k).min(doc.slides.len());
|
||||
@@ -377,7 +377,7 @@ pub fn apply_layout(doc: &mut Presentation, index: usize, layout: LayoutId) -> R
|
||||
doc.layout(layout).map(|(_, l)| l.shapes.clone()).ok_or_else(|| bad("slide.layout", "no such layout"))?;
|
||||
let mut fresh = vec![];
|
||||
for ls in lay_shapes.iter().filter(|x| x.ph.as_ref().is_some_and(|p| !p.kind.is_footer_kind())) {
|
||||
let id = deckcraft_model::ShapeId(doc.alloc_id());
|
||||
let id = deckcraft_model::ShapeId(doc.alloc_id().ok_or_else(|| bad("slide.layout", NO_ID_SPACE))?);
|
||||
fresh.push((id, ls.clone()));
|
||||
}
|
||||
let slide = doc.slides.get_mut(index).ok_or_else(|| bad("slide.layout", "no slide"))?;
|
||||
|
||||
@@ -1703,12 +1703,12 @@ fn target_group(s: &Session, p: &Value, cmd: &str) -> Result<ShapeId> {
|
||||
}
|
||||
|
||||
/// Children with fresh ids for diagram `d` inside a group whose child space is `area`.
|
||||
fn regenerate(doc: &mut Presentation, d: &Diagram, area: Xfrm) -> Vec<Shape> {
|
||||
fn regenerate(cmd: &str, doc: &mut Presentation, d: &Diagram, area: Xfrm) -> Result<Vec<Shape>> {
|
||||
let mut v = layout(d, area);
|
||||
for sh in &mut v {
|
||||
sh.id = ShapeId(doc.alloc_id());
|
||||
sh.id = ShapeId(doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?);
|
||||
}
|
||||
v
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Read the diagram (with typed text synced), let `f` change it, and regenerate the group.
|
||||
@@ -1721,7 +1721,7 @@ fn edit_diagram<T>(s: &mut Session, p: &Value, cmd: &str, f: impl FnOnce(&mut Di
|
||||
let out = f(&mut d)?;
|
||||
normalize(&mut d.nodes);
|
||||
d.imported = false;
|
||||
let kids = regenerate(doc, &d, child);
|
||||
let kids = regenerate(cmd, doc, &d, child)?;
|
||||
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?;
|
||||
let sh = deckcraft_model::find_shape_mut(shapes, gid).ok_or_else(|| bad(cmd, "no such shape"))?;
|
||||
sh.kind = ShapeKind::Group { children: kids, child };
|
||||
@@ -1747,8 +1747,8 @@ pub fn insert(s: &mut Session, kind: &str, mut nodes: Vec<DiagramNode>) -> Resul
|
||||
let size = s.doc()?.doc.slide_size;
|
||||
let area = Xfrm::new(size.width * 0.1, size.height * 0.28, size.width * 0.8, size.height * 0.6);
|
||||
let id = s.edit(|doc, sel| {
|
||||
let children = regenerate(doc, &d, area);
|
||||
let id = ShapeId(doc.alloc_id());
|
||||
let children = regenerate("insert.smartArt", doc, &d, area)?;
|
||||
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("insert.smartArt", NO_ID_SPACE))?);
|
||||
let g = Shape {
|
||||
id,
|
||||
name: format!("SmartArt ({kind})"),
|
||||
@@ -1858,8 +1858,8 @@ fn from_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let d = Diagram { kind: kind.clone(), nodes, ..Default::default() };
|
||||
let gid = s.edit(|doc, sel| {
|
||||
let children = regenerate(doc, &d, area);
|
||||
let gid = ShapeId(doc.alloc_id());
|
||||
let children = regenerate(CMD, doc, &d, area)?;
|
||||
let gid = ShapeId(doc.alloc_id().ok_or_else(|| bad(CMD, NO_ID_SPACE))?);
|
||||
let g = Shape {
|
||||
id: gid,
|
||||
name: format!("SmartArt ({kind})"),
|
||||
@@ -1914,7 +1914,7 @@ fn to_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
body.body.wrap = Some(true);
|
||||
body.body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 });
|
||||
let id = ShapeId(doc.alloc_id());
|
||||
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad(CMD, NO_ID_SPACE))?);
|
||||
let tb =
|
||||
Shape { id, name: "Text".into(), xfrm: Some(area), text_box: true, text: Some(body), geom: Geom::preset("rect"), ..Default::default() };
|
||||
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(CMD, "no slide"))?;
|
||||
@@ -1989,7 +1989,7 @@ fn set_picture(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (name, bytes) = super::insert::media_bytes(p, CMD)?;
|
||||
let ct = super::insert::content_type(&name, &bytes);
|
||||
target_group(s, p, CMD)?;
|
||||
Some(s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?)
|
||||
Some(s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad(CMD, NO_ID_SPACE)))?)
|
||||
};
|
||||
edit_diagram(s, p, CMD, |d| {
|
||||
// A sub-point's picture is its top-level node's: that's the node the placeholder shows.
|
||||
|
||||
@@ -182,7 +182,7 @@ fn sound(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let existing = doc.media.iter().find(|m| m.name == name && m.data.as_slice() == bytes.as_slice()).map(|m| m.id);
|
||||
let media = match existing {
|
||||
Some(m) => m,
|
||||
None => doc.add_media(&name, ct, bytes),
|
||||
None => doc.add_media(&name, ct, bytes).ok_or_else(|| bad("transition.sound", NO_ID_SPACE))?,
|
||||
};
|
||||
for &i in &list {
|
||||
if let Some(sl) = doc.slides.get_mut(i) {
|
||||
|
||||
@@ -138,9 +138,10 @@ fn zoom(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let boxes = grid(targets.len(), area, aspect);
|
||||
let mut shapes = vec![];
|
||||
for ((t, (png, label)), b) in targets.iter().zip(pictures).zip(boxes) {
|
||||
let media = doc.add_media(&format!("zoom-{}.png", t.index + 1), "image/png", png);
|
||||
let media = doc.add_media(&format!("zoom-{}.png", t.index + 1), "image/png", png).ok_or_else(|| bad("insert.zoom", NO_ID_SPACE))?;
|
||||
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("insert.zoom", NO_ID_SPACE))?);
|
||||
shapes.push(Shape {
|
||||
id: ShapeId(doc.alloc_id()),
|
||||
id,
|
||||
name: format!(
|
||||
"{} Zoom {}",
|
||||
if summary {
|
||||
@@ -171,7 +172,7 @@ fn zoom(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// A new Title Only slide before the first part, as PowerPoint makes it.
|
||||
let layout =
|
||||
defaults::layout_of_kind(doc, LayoutType::TitleOnly).or_else(|| doc.slides.get(first_target).map(|x| x.layout)).unwrap_or_default();
|
||||
let mut slide = defaults::new_slide(doc, layout);
|
||||
let mut slide = defaults::new_slide(doc, layout).ok_or_else(|| bad("insert.zoom", NO_ID_SPACE))?;
|
||||
if let Some(sh) = slide.shapes.iter_mut().find(|x| x.ph_type().is_some_and(|k| k.is_title())) {
|
||||
sh.text = Some(TextBody::from_text("Summary"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user