diff --git a/ROADMAP.md b/ROADMAP.md index afae9ac4..29baf55a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -237,6 +237,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-13 | WordArt adjust handles: a selected text warp shows one yellow handle per adjust, and dragging it sets that guide value (one undo step) | | 2026-10-14 | Draw: pen pressure scales ink width when the pointer reports it (a mouse or a missing reading stays at the pen width; a stroke saved as a flat 0.5 still draws at the pen width). Tilt is not read | | 2026-10-13 | Saving a presentation keeps the embedded workbook behind an unchanged chart, and the chart part's relationships (#335). Editing the chart drops that workbook instead of writing numbers that no longer match it | +| 2026-10-13 | A presentation whose object ids are used up refuses to grow instead of handing out duplicate ids (#407) | | 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless | | 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline | | 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting | diff --git a/crates/engine/src/cmd/arrange.rs b/crates/engine/src/cmd/arrange.rs index c819ab3a..75cb6fcb 100644 --- a/crates/engine/src/cmd/arrange.rs +++ b/crates/engine/src/cmd/arrange.rs @@ -189,7 +189,7 @@ fn group(s: &mut Session, p: &Value) -> Result { let xs: Vec = 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, diff --git a/crates/engine/src/cmd/artistic.rs b/crates/engine/src/cmd/artistic.rs index 8bdba6d1..071cd383 100644 --- a/crates/engine/src/cmd/artistic.rs +++ b/crates/engine/src/cmd/artistic.rs @@ -173,7 +173,10 @@ fn artistic_effect(s: &mut Session, p: &Value) -> Result { s.edit(move |doc, sel| { let mut set: Vec<(ShapeId, MediaId, Option)> = 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"))?; diff --git a/crates/engine/src/cmd/background.rs b/crates/engine/src/cmd/background.rs index ab881bc0..5ad93ad9 100644 --- a/crates/engine/src/cmd/background.rs +++ b/crates/engine/src/cmd/background.rs @@ -165,8 +165,10 @@ fn remove_background(s: &mut Session, p: &Value) -> Result { } 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::>()?; 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) diff --git a/crates/engine/src/cmd/compress.rs b/crates/engine/src/cmd/compress.rs index efa7ebfb..180391c5 100644 --- a/crates/engine/src/cmd/compress.rs +++ b/crates/engine/src/cmd/compress.rs @@ -169,14 +169,19 @@ fn compress(s: &mut Session, p: &Value) -> Result { 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 = diff --git a/crates/engine/src/cmd/design.rs b/crates/engine/src/cmd/design.rs index e2051041..03468f37 100644 --- a/crates/engine/src/cmd/design.rs +++ b/crates/engine/src/cmd/design.rs @@ -166,7 +166,7 @@ fn browse_theme(s: &mut Session, p: &Value) -> Result { // 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 { } 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 { }; 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 { }; 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 { 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 { }); 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 { fn notes_placeholders(s: &mut Session, p: &Value) -> Result { s.edit(|doc, _| { let size = doc.notes_size; - let ids: Vec = NOTES_KINDS.iter().map(|_| ShapeId(doc.alloc_id())).collect(); + let ids: Vec = + NOTES_KINDS.iter().map(|_| doc.alloc_id().map(ShapeId).ok_or_else(|| bad("notes.placeholders", NO_ID_SPACE))).collect::>()?; 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 { 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 { 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 { 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(), diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 687b1639..3b4346e1 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -184,22 +184,29 @@ fn cut(s: &mut Session, p: &Value) -> Result { /// 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 { - fn rec(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape], map: &mut HashMap, depth: usize) { +pub(crate) fn reid(cmd: &str, doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]) -> Result> { + fn rec( + cmd: &str, + doc: &mut deckcraft_model::Presentation, + shapes: &mut [Shape], + map: &mut HashMap, + 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) { /// 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 { +fn bring_media(cmd: &str, doc: &mut deckcraft_model::Presentation, media: &[deckcraft_model::MediaItem]) -> Result> { 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 { @@ -308,12 +315,12 @@ fn paste(s: &mut Session, p: &Value) -> Result { 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 { 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 { } 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 = 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 { let list = super::current_shapes(doc, sel).cloned().unwrap_or_default(); let mut copies: Vec = 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; diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index c278f1c4..149957df 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -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 { +pub(crate) fn add_shape(s: &mut Session, cmd: &str, mut shape: Shape, select: bool) -> Result { 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 { 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 { 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 { 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 { } _ => [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 { })?; 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, 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 { } } } - 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 { } 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 { }); 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 { } } } - 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 { }), ..Default::default() }; - let id = add_shape(s, shape, true)?; + let id = add_shape(s, "insert.actionButton", shape, true)?; Ok(json!({"id": id})) } diff --git a/crates/engine/src/cmd/media.rs b/crates/engine/src/cmd/media.rs index 855370ad..d9a3147d 100644 --- a/crates/engine/src/cmd/media.rs +++ b/crates/engine/src/cmd/media.rs @@ -139,7 +139,7 @@ fn poster_frame(s: &mut Session, p: &Value) -> Result { 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); diff --git a/crates/engine/src/cmd/merge.rs b/crates/engine/src/cmd/merge.rs index 24b4c3ea..45990321 100644 --- a/crates/engine/src/cmd/merge.rs +++ b/crates/engine/src/cmd/merge.rs @@ -237,7 +237,7 @@ fn merge(s: &mut Session, p: &Value) -> Result { 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"))?; diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 967b5f85..5695cc24 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -293,6 +293,10 @@ pub fn parse_params(doc: &str) -> std::result::Result>, 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) -> EngineError { EngineError::BadParams { cmd: cmd.into(), msg: msg.into() } } diff --git a/crates/engine/src/cmd/reuse.rs b/crates/engine/src/cmd/reuse.rs index 4351c35e..905f425d 100644 --- a/crates/engine/src/cmd/reuse.rs +++ b/crates/engine/src/cmd/reuse.rs @@ -61,12 +61,12 @@ fn remap_connectors(shapes: &mut [deckcraft_model::Shape], map: &HashMap HashMap { +fn renumber(doc: &mut Presentation, shapes: &mut [deckcraft_model::Shape]) -> Result> { let old = shape_ids(shapes); - super::edit::reid(doc, shapes); + super::edit::reid("slide.reuse", doc, shapes)?; let map: HashMap = 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 { 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 { 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 { /// 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) { +fn copy_media(doc: &mut Presentation, src: &Presentation, used: &[MediaId], map: &mut HashMap) -> 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> = (0..8u8).map(|i| vec![0xD0, i, i, i]).collect(); - let ids: Vec = - blobs.iter().enumerate().map(|(i, b)| src.add_media(&format!("m{i}.bin"), "application/octet-stream", b.clone())).collect(); + let ids: Vec = 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"); diff --git a/crates/engine/src/cmd/review.rs b/crates/engine/src/cmd/review.rs index 50eb4c5c..65933bbd 100644 --- a/crates/engine/src/cmd/review.rs +++ b/crates/engine/src/cmd/review.rs @@ -684,7 +684,7 @@ fn accept(s: &mut Session, p: &Value) -> Result { 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(); diff --git a/crates/engine/src/cmd/shape.rs b/crates/engine/src/cmd/shape.rs index 1f0c7bd0..5b0dc5be 100644 --- a/crates/engine/src/cmd/shape.rs +++ b/crates/engine/src/cmd/shape.rs @@ -538,7 +538,7 @@ fn fill(s: &mut Session, p: &Value) -> Result { } 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 { fn picture_change(s: &mut Session, p: &Value) -> Result { 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 { 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 { diff --git a/crates/engine/src/cmd/slide.rs b/crates/engine/src/cmd/slide.rs index deb702c3..7b3384fb 100644 --- a/crates/engine/src/cmd/slide.rs +++ b/crates/engine/src/cmd/slide.rs @@ -115,7 +115,7 @@ fn new(s: &mut Session, p: &Value) -> Result { } } }; - 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 { 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"))?; diff --git a/crates/engine/src/cmd/smartart.rs b/crates/engine/src/cmd/smartart.rs index 03276e9f..a483197c 100644 --- a/crates/engine/src/cmd/smartart.rs +++ b/crates/engine/src/cmd/smartart.rs @@ -1703,12 +1703,12 @@ fn target_group(s: &Session, p: &Value, cmd: &str) -> Result { } /// 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 { +fn regenerate(cmd: &str, doc: &mut Presentation, d: &Diagram, area: Xfrm) -> Result> { 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(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) -> 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 { }; 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 { } 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 { 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. diff --git a/crates/engine/src/cmd/transition.rs b/crates/engine/src/cmd/transition.rs index 4047faf2..327298c6 100644 --- a/crates/engine/src/cmd/transition.rs +++ b/crates/engine/src/cmd/transition.rs @@ -182,7 +182,7 @@ fn sound(s: &mut Session, p: &Value) -> Result { 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) { diff --git a/crates/engine/src/cmd/zoom.rs b/crates/engine/src/cmd/zoom.rs index fe6b92e0..039e1671 100644 --- a/crates/engine/src/cmd/zoom.rs +++ b/crates/engine/src/cmd/zoom.rs @@ -138,9 +138,10 @@ fn zoom(s: &mut Session, p: &Value) -> Result { 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 { // 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")); } diff --git a/crates/engine/src/compare.rs b/crates/engine/src/compare.rs index 5d65c0cc..93469f06 100644 --- a/crates/engine/src/compare.rs +++ b/crates/engine/src/compare.rs @@ -228,8 +228,9 @@ fn max_shape_id(shapes: &[Shape], m: &mut u32) { /// The revised slide made to live in `doc`: its media copied over (remapped on an id clash), shape /// ids kept unless another slide of `doc` uses them (then fresh, animations following), and its -/// layout matched by id, then by name, then the first layout. -pub fn adopt(doc: &mut Presentation, rev: &Presentation, slide: &Slide, replacing: Option) -> Slide { +/// layout matched by id, then by name, then the first layout. An error when `doc` has used every +/// object id and cannot take the slide without duplicating ids (#407). +pub fn adopt(doc: &mut Presentation, rev: &Presentation, slide: &Slide, replacing: Option) -> crate::Result { let mut sl = slide.clone(); // Media. let mut mids = vec![]; @@ -241,7 +242,9 @@ pub fn adopt(doc: &mut Presentation, rev: &Presentation, slide: &Slide, replacin Some(x) if x.data == m.data => {} None => doc.media.push(m.clone()), Some(_) => { - let new = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone()); + let new = doc + .add_media(&m.name, &m.content_type, m.data.as_ref().clone()) + .ok_or_else(|| crate::cmd::bad("review.accept", crate::cmd::NO_ID_SPACE))?; media_map.insert(id, new); } } @@ -261,13 +264,13 @@ pub fn adopt(doc: &mut Presentation, rev: &Presentation, slide: &Slide, replacin ids: &mut HashMap, media: &HashMap, depth: usize, - ) { + ) -> crate::Result<()> { if depth > 64 { - return; + return Ok(()); } for s in shapes { if taken.contains(&s.id.0) { - let new = ShapeId(doc.alloc_id()); + let new = ShapeId(doc.alloc_id().ok_or_else(|| crate::cmd::bad("review.accept", crate::cmd::NO_ID_SPACE))?); ids.insert(s.id, new); s.id = new; } @@ -290,11 +293,12 @@ pub fn adopt(doc: &mut Presentation, rev: &Presentation, slide: &Slide, replacin remap(&mut pf.media); } if let Some(ch) = s.children_mut() { - rec(doc, ch, taken, ids, media, depth + 1); + rec(doc, ch, taken, ids, media, depth + 1)?; } } + Ok(()) } - rec(doc, &mut sl.shapes, &taken, &mut id_map, &media_map, 0); + rec(doc, &mut sl.shapes, &taken, &mut id_map, &media_map, 0)?; for a in &mut sl.animations { if let Some(n) = id_map.get(&a.shape) { a.shape = *n; @@ -318,37 +322,39 @@ pub fn adopt(doc: &mut Presentation, rev: &Presentation, slide: &Slide, replacin Some(id) => sl.id = id, None => { if doc.slides.iter().any(|s| s.id == sl.id) || sl.id.0 == 0 { - sl.id = SlideId(doc.alloc_id()); + sl.id = SlideId(doc.alloc_id().ok_or_else(|| crate::cmd::bad("review.accept", crate::cmd::NO_ID_SPACE))?); } doc.next_id = doc.next_id.max(sl.id.0); } } - sl + Ok(sl) } -/// Apply `change` (from [`diff`] of `doc` against `rev`) to `doc`. Returns the slide index to show. -pub fn accept(doc: &mut Presentation, rev: &Presentation, change: &Change) -> Option { +/// Apply `change` (from [`diff`] of `doc` against `rev`) to `doc`. Returns the slide index to show +/// (`None` when the change no longer applies), or an error when `doc` has used every object id. +pub fn accept(doc: &mut Presentation, rev: &Presentation, change: &Change) -> crate::Result> { match change.kind { ChangeKind::Changed => { - let b = rev.slides.get(change.revised?)?.clone(); - let id = doc.slides.get(change.slide)?.id; - let sl = adopt(doc, rev, &b, Some(id)); - *doc.slides.get_mut(change.slide)? = Arc::new(sl); - Some(change.slide) + let Some(b) = change.revised.and_then(|r| rev.slides.get(r)).cloned() else { return Ok(None) }; + let Some(id) = doc.slides.get(change.slide).map(|s| s.id) else { return Ok(None) }; + let sl = adopt(doc, rev, &b, Some(id))?; + let Some(slot) = doc.slides.get_mut(change.slide) else { return Ok(None) }; + *slot = Arc::new(sl); + Ok(Some(change.slide)) } ChangeKind::Inserted => { - let b = rev.slides.get(change.revised?)?.clone(); - let sl = adopt(doc, rev, &b, None); + let Some(b) = change.revised.and_then(|r| rev.slides.get(r)).cloned() else { return Ok(None) }; + let sl = adopt(doc, rev, &b, None)?; let at = change.slide.min(doc.slides.len()); doc.slides.insert(at, Arc::new(sl)); - Some(at) + Ok(Some(at)) } ChangeKind::Deleted => { if change.slide >= doc.slides.len() || doc.slides.len() <= 1 { - return None; + return Ok(None); } doc.slides.remove(change.slide); - Some(change.slide.min(doc.slides.len().saturating_sub(1))) + Ok(Some(change.slide.min(doc.slides.len().saturating_sub(1)))) } } } diff --git a/crates/engine/src/inkshape.rs b/crates/engine/src/inkshape.rs index 593bcbc1..694440cf 100644 --- a/crates/engine/src/inkshape.rs +++ b/crates/engine/src/inkshape.rs @@ -163,7 +163,7 @@ mod tests { }; let strokes = vec![mk(&rect), mk(&zig)]; s.edit(|doc, sel| { - let id = ShapeId(doc.alloc_id()); + let id = ShapeId(doc.alloc_id().expect("id")); crate::shapes_mut(doc, sel).unwrap().push(Shape { id, name: "Ink".into(), diff --git a/crates/engine/src/templates.rs b/crates/engine/src/templates.rs index eda9aa15..73fb0e46 100644 --- a/crates/engine/src/templates.rs +++ b/crates/engine/src/templates.rs @@ -216,7 +216,7 @@ pub fn build(name: &str, size: Size) -> Result { let key = Value::String(seed.layout.to_string()); let layout = find_layout(&doc, &key, 0) .ok_or_else(|| bad("file.new", format!("template `{}` uses an unknown layout `{}`", starter.name, seed.layout)))?; - let mut slide = defaults::new_slide(&mut doc, layout); + let mut slide = defaults::new_slide(&mut doc, layout).ok_or_else(|| bad("file.new", crate::cmd::NO_ID_SPACE))?; if !seed.title.is_empty() && let Some(sh) = slide.shapes.iter_mut().find(|sh| sh.ph_type().is_some_and(|k| k.is_title())) { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 926a8fd1..1a6238ee 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1069,7 +1069,9 @@ fn an_embedded_face_is_used_when_the_machine_lacks_it() { let mut body = deckcraft_model::text::TextBody::from_text("Hello"); body.paragraphs[0].runs[0].props.font = Some("DeckCraft Colr Squares".into()); slide.shapes[0].text = Some(body); - let id = p.add_media("font1.fntdata", "application/x-fontdata", include_bytes!("../../render/tests/fixtures/colr-squares.ttf").to_vec()); + let id = p + .add_media("font1.fntdata", "application/x-fontdata", include_bytes!("../../render/tests/fixtures/colr-squares.ttf").to_vec()) + .expect("media"); p.embedded_fonts.push(deckcraft_model::EmbeddedFont { typeface: "DeckCraft Colr Squares".into(), regular: Some(id), ..Default::default() }); let data = crate::cmd::base64_encode(&crate::cmd::file::save_bytes(&p, "pptx").unwrap()); let mut s = Session::with_new(); @@ -1583,8 +1585,8 @@ mod copy_remap { fn populate(s: &mut Session, bytes: &[u8]) -> (ShapeId, ShapeId, ShapeId) { let st = s.doc_mut().unwrap(); let doc = Arc::make_mut(&mut st.doc); - let media = doc.add_media("p.png", "image/png", bytes.to_vec()); - let (p, b, c) = (ShapeId(doc.alloc_id()), ShapeId(doc.alloc_id()), ShapeId(doc.alloc_id())); + let media = doc.add_media("p.png", "image/png", bytes.to_vec()).expect("media"); + let (p, b, c) = (ShapeId(doc.alloc_id().expect("id")), ShapeId(doc.alloc_id().expect("id")), ShapeId(doc.alloc_id().expect("id"))); let x = Some(Xfrm::new(10.0, 10.0, 50.0, 50.0)); let slide = Arc::make_mut(&mut doc.slides[0]); slide.shapes.push(Shape { @@ -3454,3 +3456,28 @@ fn background_loads_are_adopted_through_a_command() { assert!(s.execute("file.adopt", &json!({})).is_err()); std::fs::remove_file(&path).ok(); } + +/// A deck whose ids are used up (a hostile file can arrive with next_id at the top of the +/// range) refuses to grow instead of handing out duplicate ids (#407). +#[test] +fn an_exhausted_id_space_refuses_growth() { + let mut s = session(); + s.execute("slide.new", &json!({})).unwrap(); + std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).next_id = u32::MAX; + let before = s.doc().unwrap().doc.clone(); + assert!(s.execute("slide.new", &json!({})).is_err(), "a new slide is refused"); + assert!(s.execute("shape.insert", &json!({"preset": "rect", "rect": [10, 10, 100, 50]})).is_err()); + assert!(s.execute("slide.duplicate", &json!({})).is_err()); + assert!(s.execute("edit.duplicate", &json!({})).is_err()); + // What the deck holds is untouched: same slides, same ids, no duplicates. + let after = s.doc().unwrap().doc.clone(); + assert_eq!(after.slides.len(), before.slides.len()); + let mut ids = vec![]; + deckcraft_model::walk(&after.slides[0].shapes, &mut |sh, _| ids.push(sh.id.0)); + ids.push(after.slides[0].id.0); + ids.push(after.next_id); + let mut sorted = ids.clone(); + sorted.sort_unstable(); + sorted.dedup(); + assert_eq!(sorted.len(), ids.len(), "no id repeats"); +} diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index dbc72dcb..f1acd9e9 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -1407,7 +1407,7 @@ impl Session { let list = cmd::current_shapes(doc, sel).cloned().unwrap_or_default(); let mut copies: Vec = list.iter().filter(|x| ids.contains(&x.id)).cloned().collect(); let boxes: Vec = copies.iter().map(|c| xfrm_of(doc, sel, c)).collect(); - cmd::edit::reid(doc, &mut copies); + cmd::edit::reid("edit.duplicate", doc, &mut copies)?; for (c, b) in copies.iter_mut().zip(boxes) { c.xfrm = Some(b); c.ph = None; @@ -1561,7 +1561,7 @@ impl Session { let id = match existing { Some(id) => id, None => { - let id = ShapeId(doc.alloc_id()); + let id = ShapeId(doc.alloc_id().ok_or_else(|| cmd::bad("ink.draw", cmd::NO_ID_SPACE))?); let size = doc.slide_size; let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("ink", "no slide"))?; list.push(Shape { diff --git a/crates/engine/tests/show_clicks.rs b/crates/engine/tests/show_clicks.rs index 6b5bea05..92da2f7f 100644 --- a/crates/engine/tests/show_clicks.rs +++ b/crates/engine/tests/show_clicks.rs @@ -11,7 +11,7 @@ use deckcraft_model::{LayoutType, Presentation, Shape, ShapeId, SlideId, default fn shape(p: &mut Presentation, name: &str, text: &str, x: f64, y: f64, w: f64, h: f64) -> Shape { Shape { - id: ShapeId(p.alloc_id()), + id: ShapeId(p.alloc_id().expect("id")), name: name.into(), xfrm: Some(Xfrm::new(x, y, w, h)), text: Some(TextBody::from_text(text)), @@ -45,7 +45,7 @@ fn disappear_on_click(trigger: ShapeId, target: ShapeId) -> Animation { fn add_slide(p: &mut Presentation, shapes: Vec, animations: Vec, advance_on_click: bool) -> SlideId { let layout = defaults::layout_of_kind(p, LayoutType::Blank).unwrap_or_default(); - let mut s = defaults::new_slide(p, layout); + let mut s = defaults::new_slide(p, layout).expect("slide"); s.shapes = shapes; s.animations = animations; s.transition = (!advance_on_click).then(|| Transition { advance_on_click: false, ..Default::default() }); @@ -136,7 +136,7 @@ impl Show { fn hide_and_reveal_deck() -> Presentation { let mut p = defaults::new_presentation(None); start(&mut p); - let s2_id = SlideId(p.alloc_id()); + let s2_id = SlideId(p.alloc_id().expect("id")); let _ = s2_id; let message = shape(&mut p, "Message", "Both hidden", 100.0, 100.0, 600.0, 300.0); let mut button = shape(&mut p, "Button", "Image hidden", 100.0, 100.0, 600.0, 300.0); diff --git a/crates/format/src/lib.rs b/crates/format/src/lib.rs index cb917a2c..679dfc7f 100644 --- a/crates/format/src/lib.rs +++ b/crates/format/src/lib.rs @@ -167,8 +167,9 @@ fn load_within(bytes: &[u8], budget: Budget) -> Result<(Presentation, Vec)>; /// Append a Title and Content slide per outline entry; the number made. fn add_outline_slides(p: &mut Presentation, slides: OutlineSlides) -> usize { let Some(lay) = defaults::layout_of_kind(p, LayoutType::TitleAndContent) else { return 0 }; - let made = slides.len(); + let mut made = 0; for (title, body) in slides { - let mut s = defaults::new_slide(p, lay); + // An exhausted id space makes no new slides: what fit is kept (#407). + let Some(mut s) = defaults::new_slide(p, lay) else { break }; if let Some(t) = s.shapes.first_mut() { t.text = Some(TextBody::from_text(&title)); } @@ -235,6 +240,7 @@ fn add_outline_slides(p: &mut Presentation, slides: OutlineSlides) -> usize { } } p.slides.push(Arc::new(s)); + made += 1; } made } @@ -524,7 +530,7 @@ mod tests { #[test] fn missing_or_corrupt_media_is_reported() { let mut p = Presentation::default(); - let id = p.add_media("a.png", "image/png", b"PICTURE-BYTES-0123456789".to_vec()); + let id = p.add_media("a.png", "image/png", b"PICTURE-BYTES-0123456789".to_vec()).expect("media"); let good = save(&p).unwrap(); assert!(load_checked(&good).unwrap().1.is_empty()); // Missing member. @@ -539,7 +545,7 @@ mod tests { assert_eq!(damaged.len(), 1, "{damaged:?}"); // Linked media saved without its bytes isn't damage. let mut l = Presentation::default(); - let lid = l.add_media("v.mp4", "video/mp4", vec![]); + let lid = l.add_media("v.mp4", "video/mp4", vec![]).expect("media"); if let Some(m) = l.media.iter_mut().find(|m| m.id == lid) { m.link = Some("file:///movies/v.mp4".into()); } @@ -594,7 +600,7 @@ mod tests { #[test] fn roundtrip_with_media() { let mut p = Presentation::default(); - let id = p.add_media("a.png", "image/png", vec![1, 2, 3, 4]); + let id = p.add_media("a.png", "image/png", vec![1, 2, 3, 4]).expect("media"); let bytes = save(&p).unwrap(); assert!(sniff(&bytes)); let q = load(&bytes).unwrap(); @@ -762,6 +768,17 @@ mod tests { assert_eq!(p.slide_size.width, 960.0); } + /// A duplicate slide in a deck whose ids are used up cannot be re-id'd: it stays as it is + /// (the engine refuses edits to such a deck) instead of a duplicated fresh id (#407). + #[test] + fn repair_keeps_a_duplicate_it_cannot_reid() { + let mut p = Presentation { next_id: u32::MAX, ..Default::default() }; + p.slides.push(p.slides[0].clone()); + repair(&mut p); + assert_eq!(p.slides.len(), 2, "the duplicate stays visible"); + assert_eq!(p.slides[0].id, p.slides[1].id, "no fresh id was invented"); + } + #[test] fn outline_roundtrip() { let mut p = defaults::blank_presentation(defaults::WIDE, Default::default(), false); diff --git a/crates/model/src/defaults.rs b/crates/model/src/defaults.rs index 5300de8b..3645f332 100644 --- a/crates/model/src/defaults.rs +++ b/crates/model/src/defaults.rs @@ -519,15 +519,20 @@ pub fn blank_presentation(size: Size, theme: Theme, title_slide: bool) -> Presen raw_ext: None, }; if title_slide { + // A fresh deck starts at id 256 and this builds a handful of ids, so the slide always + // fits; should that ever stop holding, the deck arrives without its title slide rather + // than with a duplicated id. let s = new_slide(&mut p, first_layout); - p.slides.push(Arc::new(s)); + debug_assert!(s.is_some(), "a fresh deck cannot run out of ids"); + p.slides.extend(s.map(|s| vec![Arc::new(s)]).unwrap_or_default()); } p } -/// A slide for `layout` with its placeholders instantiated (empty, inheriting position). -pub fn new_slide(p: &mut Presentation, layout: LayoutId) -> Slide { - let id = SlideId(p.alloc_id()); +/// A slide for `layout` with its placeholders instantiated (empty, inheriting position). `None` +/// when the deck has used every object id and cannot grow (#407). +pub fn new_slide(p: &mut Presentation, layout: LayoutId) -> Option { + let id = SlideId(p.alloc_id()?); let mut shapes = vec![]; let lay_shapes: Vec = p.layout(layout).map(|(_, l)| l.shapes.clone()).unwrap_or_default(); for ls in lay_shapes { @@ -535,7 +540,7 @@ pub fn new_slide(p: &mut Presentation, layout: LayoutId) -> Slide { if ph.kind.is_footer_kind() { continue; } - let sid = ShapeId(p.alloc_id()); + let sid = ShapeId(p.alloc_id()?); shapes.push(Shape { id: sid, name: ls.name.clone(), @@ -546,7 +551,7 @@ pub fn new_slide(p: &mut Presentation, layout: LayoutId) -> Slide { ..Default::default() }); } - Slide { id, layout, shapes, ..Default::default() } + Some(Slide { id, layout, shapes, ..Default::default() }) } /// Layout for a kind in the first master. diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index c62a5adb..275f9440 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -1023,10 +1023,13 @@ impl Default for Presentation { } impl Presentation { - /// A fresh id, unique across slides, shapes, layouts, masters and media in this deck. - pub fn alloc_id(&mut self) -> u32 { - self.next_id = self.next_id.max(1).saturating_add(1); - self.next_id + /// A fresh id, unique across slides, shapes, layouts, masters and media in this deck. `None` + /// once every id is in use (a hostile file can arrive that way): the deck cannot grow + /// further, and callers must refuse the change rather than duplicate ids (#407). + pub fn alloc_id(&mut self) -> Option { + let next = self.next_id.max(1).checked_add(1)?; + self.next_id = next; + Some(next) } /// Make sure `next_id` is above every id in use (after loading a file). pub fn fix_next_id(&mut self) { @@ -1082,13 +1085,13 @@ impl Presentation { self.media.iter().find(|m| m.id == id) } /// Add media bytes, reusing an identical existing item. - pub fn add_media(&mut self, name: &str, content_type: &str, data: Vec) -> MediaId { + pub fn add_media(&mut self, name: &str, content_type: &str, data: Vec) -> Option { if let Some(m) = self.media.iter().find(|m| m.data.len() == data.len() && *m.data == data) { - return m.id; + return Some(m.id); } - let id = MediaId(self.alloc_id()); + let id = MediaId(self.alloc_id()?); self.media.push(MediaItem { id, name: name.into(), content_type: content_type.into(), data: Arc::new(data), link: None }); - id + Some(id) } /// Section index containing a slide (by position), when sections exist. pub fn section_of(&self, slide: SlideId) -> Option { diff --git a/crates/model/src/tests.rs b/crates/model/src/tests.rs index d7b49ce4..da234d55 100644 --- a/crates/model/src/tests.rs +++ b/crates/model/src/tests.rs @@ -37,7 +37,7 @@ fn placeholder_inherits_position_and_styles() { fn body_levels_from_master() { let mut p = Presentation::default(); let lay = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap(); - let s = defaults::new_slide(&mut p, lay); + let s = defaults::new_slide(&mut p, lay).expect("slide"); let ctx = Ctx::for_slide(&p, &s).unwrap(); let body = &s.shapes[1]; assert_eq!(body.ph_type(), Some(PhType::Obj)); @@ -97,16 +97,28 @@ fn serde_roundtrip() { assert!(q.slides.is_empty()); } +/// A deck whose ids are used up (a hostile file can arrive that way) yields no further ids: +/// allocating never repeats one (#407). +#[test] +fn exhausted_id_space_allocates_nothing() { + let mut p = Presentation { next_id: u32::MAX, ..Default::default() }; + assert_eq!(p.alloc_id(), None); + assert_eq!(p.alloc_id(), None); + assert!(p.add_media("x.png", "image/png", vec![9]).is_none(), "fresh media bytes are refused too"); + let lay = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap_or_default(); + assert!(defaults::new_slide(&mut p, lay).is_none(), "a new slide is refused"); +} + #[test] fn ids_are_unique_and_fix_next_id() { let mut p = Presentation::default(); - let a = p.alloc_id(); - let b = p.alloc_id(); + let a = p.alloc_id().expect("ids"); + let b = p.alloc_id().expect("ids"); assert!(b > a); p.next_id = 0; p.fix_next_id(); assert!(p.next_id >= b.min(p.next_id)); - let c = p.alloc_id(); + let c = p.alloc_id().expect("ids"); let mut all = vec![]; for s in &p.slides { all.push(s.id.0); diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index 59de23e2..f78b2bf9 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -7,7 +7,7 @@ fn deck(n: usize) -> Presentation { let mut p = deckcraft_model::defaults::new_presentation(None); let layout = p.slides[0].layout; while p.slides.len() < n { - let s = deckcraft_model::defaults::new_slide(&mut p, layout); + let s = deckcraft_model::defaults::new_slide(&mut p, layout).expect("slide"); p.slides.push(Arc::new(s)); } for (i, s) in p.slides.iter_mut().enumerate() { diff --git a/crates/pptx/src/write/mod.rs b/crates/pptx/src/write/mod.rs index dd222d3b..bea8dc64 100644 --- a/crates/pptx/src/write/mod.rs +++ b/crates/pptx/src/write/mod.rs @@ -501,8 +501,11 @@ fn sanitized(p: &Presentation) -> std::borrow::Cow<'_, Presentation> { } for m in q.masters.iter_mut() { if m.layouts.is_empty() { - let id = LayoutId(q.next_id.saturating_add(1)); - q.next_id = id.0; + // An exhausted id space exports without the synthesized layout rather than with a + // duplicated one (#407). + let Some(raw) = q.next_id.max(1).checked_add(1) else { continue }; + let id = LayoutId(raw); + q.next_id = raw; Arc::make_mut(m).layouts.push(Layout { id, name: "Blank".into(), diff --git a/crates/pptx/tests/chart_embed.rs b/crates/pptx/tests/chart_embed.rs index d80d4d6d..2549cdb6 100644 --- a/crates/pptx/tests/chart_embed.rs +++ b/crates/pptx/tests/chart_embed.rs @@ -14,8 +14,9 @@ fn chart_pptx() -> Vec { let mut p = defaults::new_presentation(None); p.slides.clear(); let layout = defaults::layout_of_kind(&p, LayoutType::Blank).unwrap_or_default(); - let mut slide = defaults::new_slide(&mut p, layout); - let mut shape = Shape { id: ShapeId(p.alloc_id()), name: "Pie".into(), xfrm: Some(Xfrm::new(40.0, 40.0, 400.0, 300.0)), ..Default::default() }; + let mut slide = defaults::new_slide(&mut p, layout).expect("slide"); + let mut shape = + Shape { id: ShapeId(p.alloc_id().expect("id")), name: "Pie".into(), xfrm: Some(Xfrm::new(40.0, 40.0, 400.0, 300.0)), ..Default::default() }; shape.kind = ShapeKind::Chart(Box::new(Chart::sample(ChartType::Pie))); slide.shapes.push(shape); p.slides.push(Arc::new(slide)); @@ -190,14 +191,16 @@ fn kept_chart_parts_never_collide_with_written_parts() { let picture = png([10, 200, 30]); let mut p = defaults::new_presentation(None); p.slides.clear(); - let img = p.add_media("image1.png", "image/png", picture.clone()); + let img = p.add_media("image1.png", "image/png", picture.clone()).expect("media"); let layout = defaults::layout_of_kind(&p, LayoutType::Blank).unwrap_or_default(); - let mut slide = defaults::new_slide(&mut p, layout); + let mut slide = defaults::new_slide(&mut p, layout).expect("slide"); // The chart comes first, so its kept parts are written before the picture. - let mut chart = Shape { id: ShapeId(p.alloc_id()), name: "Pie".into(), xfrm: Some(Xfrm::new(40.0, 40.0, 400.0, 300.0)), ..Default::default() }; + let mut chart = + Shape { id: ShapeId(p.alloc_id().expect("id")), name: "Pie".into(), xfrm: Some(Xfrm::new(40.0, 40.0, 400.0, 300.0)), ..Default::default() }; chart.kind = ShapeKind::Chart(Box::new(Chart::sample(ChartType::Pie))); slide.shapes.push(chart); - let mut pic = Shape { id: ShapeId(p.alloc_id()), name: "Pic".into(), xfrm: Some(Xfrm::new(500.0, 40.0, 80.0, 80.0)), ..Default::default() }; + let mut pic = + Shape { id: ShapeId(p.alloc_id().expect("id")), name: "Pic".into(), xfrm: Some(Xfrm::new(500.0, 40.0, 80.0, 80.0)), ..Default::default() }; pic.kind = ShapeKind::Picture { fill: deckcraft_model::style::PictureFill { media: img, ..Default::default() } }; slide.shapes.push(pic); p.slides.push(Arc::new(slide)); @@ -286,8 +289,9 @@ fn an_unchanged_chartex_keeps_its_embedded_workbook() { let mut p = defaults::new_presentation(None); p.slides.clear(); let layout = defaults::layout_of_kind(&p, LayoutType::Blank).unwrap_or_default(); - let mut slide = defaults::new_slide(&mut p, layout); - let mut shape = Shape { id: ShapeId(p.alloc_id()), name: "Pie".into(), xfrm: Some(Xfrm::new(40.0, 40.0, 400.0, 300.0)), ..Default::default() }; + let mut slide = defaults::new_slide(&mut p, layout).expect("slide"); + let mut shape = + Shape { id: ShapeId(p.alloc_id().expect("id")), name: "Pie".into(), xfrm: Some(Xfrm::new(40.0, 40.0, 400.0, 300.0)), ..Default::default() }; shape.kind = ShapeKind::Chart(Box::new(Chart::sample(ChartType::Waterfall))); slide.shapes.push(shape); p.slides.push(Arc::new(slide)); diff --git a/crates/pptx/tests/comments.rs b/crates/pptx/tests/comments.rs index 2aace626..c3befe09 100644 --- a/crates/pptx/tests/comments.rs +++ b/crates/pptx/tests/comments.rs @@ -127,7 +127,7 @@ fn comments_on_several_slides_round_trip() { let mut p = deck(); let layout = p.slides[0].layout; while p.slides.len() < 3 { - let s = defaults::new_slide(&mut p, layout); + let s = defaults::new_slide(&mut p, layout).expect("slide"); p.slides.push(Arc::new(s)); } let s = Arc::make_mut(&mut p.slides[2]); diff --git a/crates/pptx/tests/corpus.rs b/crates/pptx/tests/corpus.rs index 7eb37700..4f8a2b00 100644 --- a/crates/pptx/tests/corpus.rs +++ b/crates/pptx/tests/corpus.rs @@ -22,7 +22,7 @@ fn png(w: u32, h: u32, rgb: [u8; 3]) -> Vec { fn shape(p: &mut Presentation, name: &str, x: f64, y: f64, w: f64, h: f64) -> deckcraft_model::Shape { deckcraft_model::Shape { - id: deckcraft_model::ShapeId(p.alloc_id()), + id: deckcraft_model::ShapeId(p.alloc_id().expect("id")), name: name.into(), xfrm: Some(deckcraft_geom::Xfrm::new(x, y, w, h)), ..Default::default() @@ -31,7 +31,7 @@ fn shape(p: &mut Presentation, name: &str, x: f64, y: f64, w: f64, h: f64) -> de fn new_slide(p: &mut Presentation, title: &str) -> Slide { let layout = defaults::layout_of_kind(p, deckcraft_model::LayoutType::TitleOnly).unwrap_or_default(); - let mut s = defaults::new_slide(p, layout); + let mut s = defaults::new_slide(p, layout).expect("slide"); if let Some(t) = s.shapes.first_mut() { t.text = Some(TextBody::from_text(title)); } @@ -44,7 +44,7 @@ fn talk() -> Presentation { p.props.title = "Quarterly review".into(); p.props.subject = "Owned corpus".into(); p.props.author = "DeckCraft tests".into(); - let img = p.add_media("mark.png", "image/png", png(4, 4, [20, 90, 160])); + let img = p.add_media("mark.png", "image/png", png(4, 4, [20, 90, 160])).expect("media"); { let id = p.slides[0].id; diff --git a/crates/pptx/tests/corpus_charts.rs b/crates/pptx/tests/corpus_charts.rs index 210a4838..ee3faffd 100644 --- a/crates/pptx/tests/corpus_charts.rs +++ b/crates/pptx/tests/corpus_charts.rs @@ -16,7 +16,7 @@ use deckcraft_model::text::{AutoFit, TextBody, TextDir}; use deckcraft_model::{Presentation, Shape, ShapeId, ShapeKind, Slide, defaults}; fn shape(p: &mut Presentation, name: &str, x: f64, y: f64, w: f64, h: f64) -> Shape { - Shape { id: ShapeId(p.alloc_id()), name: name.into(), xfrm: Some(Xfrm::new(x, y, w, h)), ..Default::default() } + Shape { id: ShapeId(p.alloc_id().expect("id")), name: name.into(), xfrm: Some(Xfrm::new(x, y, w, h)), ..Default::default() } } fn round(p: &Presentation) -> Presentation { @@ -41,7 +41,7 @@ fn corpus() -> Presentation { p.props.title = "Chart and table corpus".into(); let mut s = { let layout = defaults::layout_of_kind(&p, deckcraft_model::LayoutType::Blank).unwrap_or_default(); - defaults::new_slide(&mut p, layout) + defaults::new_slide(&mut p, layout).expect("slide") }; let chart = Chart { diff --git a/crates/pptx/tests/corpus_elements.rs b/crates/pptx/tests/corpus_elements.rs index 1c871b8a..7d3a41da 100644 --- a/crates/pptx/tests/corpus_elements.rs +++ b/crates/pptx/tests/corpus_elements.rs @@ -18,7 +18,7 @@ fn link(action: Action) -> Hyperlink { } fn box_at(p: &mut Presentation, name: &str, x: f64, y: f64) -> Shape { - Shape { id: ShapeId(p.alloc_id()), name: name.into(), xfrm: Some(Xfrm::new(x, y, 80.0, 28.0)), ..Default::default() } + Shape { id: ShapeId(p.alloc_id().expect("id")), name: name.into(), xfrm: Some(Xfrm::new(x, y, 80.0, 28.0)), ..Default::default() } } fn round(p: &Presentation) -> Presentation { @@ -51,8 +51,8 @@ fn deck() -> Presentation { body.list_style = ls; } - let chime = p.add_media("chime.wav", "audio/wav", CHIME.to_vec()); - let mut a = defaults::new_slide(&mut p, layout); + let chime = p.add_media("chime.wav", "audio/wav", CHIME.to_vec()).expect("media"); + let mut a = defaults::new_slide(&mut p, layout).expect("slide"); a.name = "Review".into(); if let Some(title) = a.shapes.iter_mut().find(|s| s.ph_type() == Some(PhType::Title)) { title.text = Some(TextBody::from_text("Review")); @@ -165,7 +165,7 @@ fn deck() -> Presentation { ..Default::default() }]; - let mut b = defaults::new_slide(&mut p, layout); + let mut b = defaults::new_slide(&mut p, layout).expect("slide"); if let Some(title) = b.shapes.iter_mut().find(|s| s.ph_type() == Some(PhType::Title)) { title.text = Some(TextBody::from_text("Second")); } diff --git a/crates/pptx/tests/corpus_kinds.rs b/crates/pptx/tests/corpus_kinds.rs index 9b00d8b9..56fc5adf 100644 --- a/crates/pptx/tests/corpus_kinds.rs +++ b/crates/pptx/tests/corpus_kinds.rs @@ -23,7 +23,7 @@ fn png(w: u32, h: u32, rgb: [u8; 3]) -> Vec { fn shape(p: &mut Presentation, name: &str, x: f64, y: f64, w: f64, h: f64) -> deckcraft_model::Shape { deckcraft_model::Shape { - id: deckcraft_model::ShapeId(p.alloc_id()), + id: deckcraft_model::ShapeId(p.alloc_id().expect("id")), name: name.into(), xfrm: Some(deckcraft_geom::Xfrm::new(x, y, w, h)), ..Default::default() @@ -32,7 +32,7 @@ fn shape(p: &mut Presentation, name: &str, x: f64, y: f64, w: f64, h: f64) -> de fn slide_with(p: &mut Presentation, kind: LayoutType, title: &str) -> Slide { let layout = defaults::layout_of_kind(p, kind).unwrap_or_default(); - let mut s = defaults::new_slide(p, layout); + let mut s = defaults::new_slide(p, layout).expect("slide"); if let Some(t) = s.shapes.first_mut() { t.text = Some(TextBody::from_text(title)); } @@ -125,9 +125,9 @@ fn animations() -> Presentation { fn media() -> Presentation { let mut p = defaults::new_presentation(None); - let picture = p.add_media("mark.png", "image/png", png(4, 4, [20, 90, 160])); - let tone = p.add_media("tone.wav", "audio/wav", b"owned-wav-tone".to_vec()); - let clip = p.add_media("clip.mp4", "video/mp4", b"owned-mp4-clip".to_vec()); + let picture = p.add_media("mark.png", "image/png", png(4, 4, [20, 90, 160])).expect("media"); + let tone = p.add_media("tone.wav", "audio/wav", b"owned-wav-tone".to_vec()).expect("media"); + let clip = p.add_media("clip.mp4", "video/mp4", b"owned-mp4-clip".to_vec()).expect("media"); let mut s = slide_with(&mut p, LayoutType::TitleOnly, "Media"); let mut pic = shape(&mut p, "Mark", 60.0, 120.0, 80.0, 80.0); pic.kind = ShapeKind::Picture { fill: PictureFill { media: picture, ..Default::default() } }; @@ -186,14 +186,14 @@ fn masters() -> Presentation { } } let mut evening = (**p.masters.first().expect("master")).clone(); - evening.id = MasterId(p.alloc_id()); + evening.id = MasterId(p.alloc_id().expect("id")); evening.name = "Evening".into(); evening.shapes.retain(|s| s.ph_type().is_some()); let mut banner = shape(&mut p, "Evening banner", 40.0, 40.0, 400.0, 40.0); banner.text = Some(TextBody::from_text("Evening master")); evening.shapes.push(banner); let mut quote = evening.layouts.first().expect("layout").clone(); - quote.id = LayoutId(p.alloc_id()); + quote.id = LayoutId(p.alloc_id().expect("id")); quote.name = "Quote".into(); quote.kind = LayoutType::Custom; quote.shapes.retain(|s| s.ph_type() == Some(PhType::CtrTitle) || s.ph_type() == Some(PhType::Title)); @@ -207,7 +207,7 @@ fn masters() -> Presentation { let title = slide_with(&mut p, LayoutType::Title, "Welcome"); let agenda = slide_with(&mut p, LayoutType::SectionHeader, "Agenda"); let compared = slide_with(&mut p, LayoutType::Comparison, "Comparison"); - let mut quoted = defaults::new_slide(&mut p, quote_id); + let mut quoted = defaults::new_slide(&mut p, quote_id).expect("slide"); if let Some(t) = quoted.shapes.first_mut() { t.text = Some(TextBody::from_text("A line")); } diff --git a/crates/pptx/tests/corpus_media.rs b/crates/pptx/tests/corpus_media.rs index c03f16d3..6ffd14aa 100644 --- a/crates/pptx/tests/corpus_media.rs +++ b/crates/pptx/tests/corpus_media.rs @@ -26,12 +26,12 @@ fn glb(marker: u8) -> Vec { } fn shape(p: &mut Presentation, name: &str, x: f64, y: f64, w: f64, h: f64) -> Shape { - Shape { id: ShapeId(p.alloc_id()), name: name.into(), xfrm: Some(Xfrm::new(x, y, w, h)), ..Default::default() } + Shape { id: ShapeId(p.alloc_id().expect("id")), name: name.into(), xfrm: Some(Xfrm::new(x, y, w, h)), ..Default::default() } } fn new_slide(p: &mut Presentation, title: &str) -> Slide { let layout = defaults::layout_of_kind(p, deckcraft_model::LayoutType::TitleOnly).unwrap_or_default(); - let mut s = defaults::new_slide(p, layout); + let mut s = defaults::new_slide(p, layout).expect("slide"); if let Some(t) = s.shapes.first_mut() { t.text = Some(TextBody::from_text(title)); } @@ -55,11 +55,11 @@ fn corpus() -> (Presentation, Vec, Vec, Vec) { let block = glb(1); let globe = glb(2); let poster = png([20, 80, 160]); - let audio = p.add_media("clip.m4a", "audio/mp4", audio_bytes); - let video = p.add_media("clip.mp4", "video/mp4", video_bytes); - let block_id = p.add_media("block.glb", "model/gltf-binary", block.clone()); - let globe_id = p.add_media("globe.glb", "model/gltf-binary", globe.clone()); - let poster_id = p.add_media("globe-poster.png", "image/png", poster.clone()); + let audio = p.add_media("clip.m4a", "audio/mp4", audio_bytes).expect("media"); + let video = p.add_media("clip.mp4", "video/mp4", video_bytes).expect("media"); + let block_id = p.add_media("block.glb", "model/gltf-binary", block.clone()).expect("media"); + let globe_id = p.add_media("globe.glb", "model/gltf-binary", globe.clone()).expect("media"); + let poster_id = p.add_media("globe-poster.png", "image/png", poster.clone()).expect("media"); let mut objects = new_slide(&mut p, "Objects"); let mut diagram = shape(&mut p, "Diagram", 40.0, 80.0, 288.0, 144.0); diff --git a/crates/pptx/tests/corpus_style.rs b/crates/pptx/tests/corpus_style.rs index 9f02c641..65c3631c 100644 --- a/crates/pptx/tests/corpus_style.rs +++ b/crates/pptx/tests/corpus_style.rs @@ -10,7 +10,7 @@ use deckcraft_model::text::{Align, AutoFit, BodyProps, ParaProps, Paragraph, Run use deckcraft_model::{Geom, Presentation, Shape, ShapeId, ShapeKind, defaults}; fn shape(p: &mut Presentation, name: &str, x: f64, y: f64, w: f64, h: f64) -> Shape { - Shape { id: ShapeId(p.alloc_id()), name: name.into(), xfrm: Some(Xfrm::new(x, y, w, h)), ..Default::default() } + Shape { id: ShapeId(p.alloc_id().expect("id")), name: name.into(), xfrm: Some(Xfrm::new(x, y, w, h)), ..Default::default() } } fn shadow(inner: bool, dir: f64) -> Shadow { @@ -74,7 +74,7 @@ fn style_deck() -> Presentation { }; let layout = defaults::layout_of_kind(&p, deckcraft_model::LayoutType::Blank).unwrap_or_default(); - let mut s = defaults::new_slide(&mut p, layout); + let mut s = defaults::new_slide(&mut p, layout).expect("slide"); let mut word = shape(&mut p, "Styled text", 40.0, 40.0, 400.0, 180.0); word.text = Some(TextBody { diff --git a/crates/pptx/tests/corpus_themes.rs b/crates/pptx/tests/corpus_themes.rs index 80c41d20..15f76ae0 100644 --- a/crates/pptx/tests/corpus_themes.rs +++ b/crates/pptx/tests/corpus_themes.rs @@ -64,7 +64,7 @@ fn effects_and_links() -> Vec { let mut p = defaults::new_presentation(None); let layout = defaults::layout_of_kind(&p, LayoutType::Blank).unwrap_or_default(); for _ in 0..2 { - let s = defaults::new_slide(&mut p, layout); + let s = defaults::new_slide(&mut p, layout).expect("slide"); p.slides.push(Arc::new(s)); } p.slides.truncate(2); @@ -177,7 +177,7 @@ fn effects_and_links_survive_a_round_trip() { fn two_masters() -> Presentation { let mut p = defaults::new_presentation(None); let mut second = (*p.masters[0]).clone(); - second.id = deckcraft_model::MasterId(p.alloc_id()); + second.id = deckcraft_model::MasterId(p.alloc_id().expect("id")); second.name = "Brand Dark".into(); second.theme.name = "Brand Dark Theme".into(); second.theme.colors.name = "Brand Colours".into(); @@ -190,15 +190,15 @@ fn two_masters() -> Presentation { fill: deckcraft_model::Fill::solid(deckcraft_model::ColorRef::rgb(deckcraft_model::Rgba::rgb(0x10, 0x10, 0x18))), }); for l in &mut second.layouts { - l.id = deckcraft_model::LayoutId(p.alloc_id()); + l.id = deckcraft_model::LayoutId(p.alloc_id().expect("id")); l.name = format!("Dark {}", l.name); } second.layouts.truncate(4); let dark_layout = second.layouts[1].id; p.masters.push(Arc::new(second)); let light_layout = p.masters[0].layouts[0].id; - let s1 = defaults::new_slide(&mut p, light_layout); - let s2 = defaults::new_slide(&mut p, dark_layout); + let s1 = defaults::new_slide(&mut p, light_layout).expect("slide"); + let s2 = defaults::new_slide(&mut p, dark_layout).expect("slide"); p.slides = vec![Arc::new(s1), Arc::new(s2)]; p } diff --git a/crates/pptx/tests/fills.rs b/crates/pptx/tests/fills.rs index e5955c27..98069d11 100644 --- a/crates/pptx/tests/fills.rs +++ b/crates/pptx/tests/fills.rs @@ -36,8 +36,8 @@ fn png() -> Vec { /// 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 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, @@ -86,7 +86,7 @@ fn a_gradient_without_stops_is_saved_without_stops() { // must not turn it into a gradient of its own (it used to become solid white). use deckcraft_model::style::{Gradient, GradientShape}; let mut p = defaults::new_presentation(None); - let id = ShapeId(p.alloc_id()); + let id = ShapeId(p.alloc_id().expect("ids")); let first = p.slides[0].id; let own = Gradient { tile: [0.0; 4], stops: vec![], shape: GradientShape::Linear { angle: 45.0, scaled: true }, rotate_with_shape: true }; p.slide_mut(first).expect("slide").shapes.push(Shape { @@ -107,7 +107,7 @@ fn a_gradient_without_stops_is_saved_without_stops() { fn a_fill_overlay_round_trips() { use deckcraft_model::style::{FillOverlay, Gradient, GradientShape, GradientStop}; let mut p = defaults::new_presentation(None); - let id = ShapeId(p.alloc_id()); + let id = ShapeId(p.alloc_id().expect("ids")); let first = p.slides[0].id; let overlay = FillOverlay { blend: "mult".into(), @@ -150,7 +150,7 @@ fn a_gradient_tile_rect_is_read_and_round_trips() { tile: [-1.0, -1.0, 0.0, 0.0], }; let mut p = defaults::new_presentation(None); - let id = ShapeId(p.alloc_id()); + let id = ShapeId(p.alloc_id().expect("ids")); let first = p.slides[0].id; p.slide_mut(first).expect("slide").shapes.push(Shape { id, @@ -174,7 +174,7 @@ fn a_gradient_tile_rect_is_read_and_round_trips() { #[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()); + 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(); @@ -207,8 +207,8 @@ fn a_table_naming_no_style_is_drawn_with_the_table_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()); - let id = ShapeId(p.alloc_id()); + 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); diff --git a/crates/pptx/tests/locks.rs b/crates/pptx/tests/locks.rs index 2e903e60..60e0d874 100644 --- a/crates/pptx/tests/locks.rs +++ b/crates/pptx/tests/locks.rs @@ -15,14 +15,14 @@ fn png() -> Vec { } fn shape(p: &mut Presentation, name: &str, x: f64) -> Shape { - Shape { id: ShapeId(p.alloc_id()), name: name.into(), xfrm: Some(Xfrm::new(x, 40.0, 80.0, 60.0)), ..Default::default() } + 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()); + 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; @@ -40,7 +40,7 @@ fn deck() -> Presentation { c3.hidden = true; let gx = Xfrm::new(300.0, 40.0, 280.0, 60.0); let mut g = Shape { - id: ShapeId(p.alloc_id()), + id: ShapeId(p.alloc_id().expect("ids")), name: "Group".into(), xfrm: Some(gx), kind: ShapeKind::Group { children: vec![c1, c2, c3], child: gx }, diff --git a/crates/pptx/tests/math_ns.rs b/crates/pptx/tests/math_ns.rs index f5cc6bec..89b0ac2b 100644 --- a/crates/pptx/tests/math_ns.rs +++ b/crates/pptx/tests/math_ns.rs @@ -34,7 +34,7 @@ fn rezip(es: &[(String, Vec)]) -> Vec { /// return the OMML read back. fn moved_to(host: &str) -> String { let mut p = defaults::new_presentation(None); - let id = ShapeId(p.alloc_id()); + let id = ShapeId(p.alloc_id().expect("id")); let omml = format!("x=1"); let eq = Shape { id, diff --git a/crates/pptx/tests/roundtrip.rs b/crates/pptx/tests/roundtrip.rs index b6a5e02d..697dad20 100644 --- a/crates/pptx/tests/roundtrip.rs +++ b/crates/pptx/tests/roundtrip.rs @@ -27,12 +27,12 @@ fn png(w: u32, h: u32, rgb: [u8; 3]) -> Vec { } fn shape(p: &mut Presentation, name: &str, x: f64, y: f64, w: f64, h: f64) -> Shape { - Shape { id: ShapeId(p.alloc_id()), name: name.into(), xfrm: Some(Xfrm::new(x, y, w, h)), ..Default::default() } + Shape { id: ShapeId(p.alloc_id().expect("ids")), name: name.into(), xfrm: Some(Xfrm::new(x, y, w, h)), ..Default::default() } } fn new_slide(p: &mut Presentation, title: &str) -> Slide { let layout = defaults::layout_of_kind(p, deckcraft_model::LayoutType::TitleOnly).unwrap_or_default(); - let mut s = defaults::new_slide(p, layout); + let mut s = defaults::new_slide(p, layout).expect("slide"); if let Some(t) = s.shapes.first_mut() { t.text = Some(TextBody::from_text(title)); } @@ -45,9 +45,9 @@ pub fn rich_deck() -> Presentation { p.props.title = "Rich deck".into(); p.props.author = "DeckCraft tests".into(); p.props.created = "2025-01-02T03:04:05Z".into(); - let img = p.add_media("image1.png", "image/png", png(40, 30, [200, 40, 40])); - let img2 = p.add_media("image2.png", "image/png", png(8, 8, [20, 140, 220])); - let video = p.add_media("clip.mp4", "video/mp4", b"\x00\x00\x00\x18ftypmp42not-a-real-video".to_vec()); + let img = p.add_media("image1.png", "image/png", png(40, 30, [200, 40, 40])).expect("media"); + let img2 = p.add_media("image2.png", "image/png", png(8, 8, [20, 140, 220])).expect("media"); + let video = p.add_media("clip.mp4", "video/mp4", b"\x00\x00\x00\x18ftypmp42not-a-real-video".to_vec()).expect("media"); // Slide 1: title slide with notes and formatted text. { @@ -339,7 +339,7 @@ pub fn rich_deck() -> Presentation { // Slide 6: bulleted content with a by-paragraph build, a slide link and an equation. let layout = defaults::layout_of_kind(&p, deckcraft_model::LayoutType::TitleAndContent).unwrap_or_default(); - let mut s = defaults::new_slide(&mut p, layout); + let mut s = defaults::new_slide(&mut p, layout).expect("slide"); s.shapes[0].text = Some(TextBody::from_text("Build")); s.shapes[1].text = Some(TextBody::from_text("One\nTwo\nThree")); let body = s.shapes[1].id; @@ -517,7 +517,7 @@ fn pictures_media_and_ink_round_trip() { #[test] fn pressured_ink_exports_at_its_mean_width() { let mut p = Presentation::default(); - let id = ShapeId(p.alloc_id()); + let id = ShapeId(p.alloc_id().expect("id")); let slide = std::sync::Arc::make_mut(&mut p.slides[0]); slide.shapes.push(Shape { id, @@ -638,7 +638,7 @@ fn every_transition_kind_survives() { p.slides.clear(); for (kind, ..) in deckcraft_model::anim::TRANSITIONS { let mut s = base.clone(); - s.id = SlideId(p.alloc_id()); + s.id = SlideId(p.alloc_id().expect("ids")); s.transition = Some(Transition { kind: kind.to_string(), duration_ms: 1200, ..Default::default() }); p.slides.push(Arc::new(s)); } @@ -820,7 +820,7 @@ fn colour_map_overrides_round_trip() { #[test] fn a_picture_duotone_round_trips() { let mut p = defaults::new_presentation(None); - let img = p.add_media("image1.png", "image/png", png(4, 4, [128, 128, 128])); + let img = p.add_media("image1.png", "image/png", png(4, 4, [128, 128, 128])).expect("media"); let mut pic = shape(&mut p, "Duotone", 10.0, 10.0, 100.0, 100.0); let mut pf = PictureFill { media: img, ..Default::default() }; pf.adjust.duotone = Some((ColorRef::scheme(SchemeSlot::Accent1).with(ColorTransform::Shade(30000)), ColorRef::rgb(Rgba::rgb(250, 240, 200)))); @@ -933,7 +933,7 @@ fn transition_deck(list: &[(&str, &str, u32, Option)]) -> (Presentation, Ve p.slides.clear(); for (kind, option, ms, adv) in list { let mut s = base.clone(); - s.id = SlideId(p.alloc_id()); + s.id = SlideId(p.alloc_id().expect("ids")); s.transition = Some(Transition { kind: kind.to_string(), option: option.to_string(), @@ -1157,8 +1157,8 @@ fn float_down_is_preset_47_both_ways() { #[test] fn embedded_fonts_round_trip() { let mut p = Presentation::default(); - let regular = p.add_media("font1.fntdata", "application/x-fontdata", b"REGULAR-FONT-BYTES-0123456789".to_vec()); - let bold = p.add_media("font2.fntdata", "application/x-fontdata", b"BOLD-FONT-BYTES-0123456789abcd".to_vec()); + let regular = p.add_media("font1.fntdata", "application/x-fontdata", b"REGULAR-FONT-BYTES-0123456789".to_vec()).expect("media"); + let bold = p.add_media("font2.fntdata", "application/x-fontdata", b"BOLD-FONT-BYTES-0123456789abcd".to_vec()).expect("media"); p.embedded_fonts.push(deckcraft_model::EmbeddedFont { typeface: "Deck Test Face".into(), panose: Some("020B0604020202020204".into()), @@ -1812,11 +1812,13 @@ fn smartart_without_a_drawing_keeps_its_parts() { "data1.xml", "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml", br#"Manager"#.to_vec(), - ); + ).expect("media"); let mut parts = vec![("rId2".to_string(), "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData".to_string(), data)]; for (i, kind) in ["Layout", "QuickStyle", "Colors"].into_iter().enumerate() { let xml = format!(r#""#).into_bytes(); - let m = p.add_media(&format!("{kind}1.xml"), &format!("application/vnd.openxmlformats-officedocument.drawingml.diagram{kind}+xml"), xml); + let m = p + .add_media(&format!("{kind}1.xml"), &format!("application/vnd.openxmlformats-officedocument.drawingml.diagram{kind}+xml"), xml) + .expect("media"); parts.push((format!("rId{}", i + 3), format!("http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagram{kind}"), m)); } let xml = r#""#; @@ -1841,8 +1843,10 @@ fn smartart_without_a_drawing_keeps_its_parts() { #[test] fn legacy_ole_object_is_written_in_the_fallback_form() { let mut p = defaults::new_presentation(None); - let bin = p.add_media("oleObject1.bin", "application/vnd.openxmlformats-officedocument.oleObject", b"\xD0\xCF\x11\xE0not-a-real-ole".to_vec()); - let prev = p.add_media("image1.png", "image/png", png(4, 3, [10, 20, 30])); + let bin = p + .add_media("oleObject1.bin", "application/vnd.openxmlformats-officedocument.oleObject", b"\xD0\xCF\x11\xE0not-a-real-ole".to_vec()) + .expect("media"); + let prev = p.add_media("image1.png", "image/png", png(4, 3, [10, 20, 30])).expect("media"); let xml = r#""#; let parts = vec![("rId4".to_string(), "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject".to_string(), bin)]; let mut s = new_slide(&mut p, "OLE"); diff --git a/crates/pptx/tests/table_styles.rs b/crates/pptx/tests/table_styles.rs index 27fca053..311992df 100644 --- a/crates/pptx/tests/table_styles.rs +++ b/crates/pptx/tests/table_styles.rs @@ -20,7 +20,7 @@ fn style_xml() -> String { /// An exported deck with a 3x2 table using the probe style. fn table_deck() -> Vec { let mut p = defaults::new_presentation(None); - let id = ShapeId(p.alloc_id()); + let id = ShapeId(p.alloc_id().expect("ids")); let first = p.slides[0].id; let mut t = deckcraft_model::Table::new(3, 2, 300.0, 30.0); t.style = "medium2-accent1".into(); diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index f72ad549..c85a1e29 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -656,7 +656,7 @@ fn two_frame_gif() -> Vec { #[test] fn animated_gif_shows_the_frame_for_its_time() { let mut p = Presentation::default(); - let media = p.add_media("a.gif", "image/gif", two_frame_gif()); + let media = p.add_media("a.gif", "image/gif", two_frame_gif()).expect("media"); let fill = deckcraft_model::style::PictureFill { media, ..Default::default() }; let sh = Shape { id: ShapeId(600), xfrm: Some(Xfrm::new(0.0, 0.0, 960.0, 540.0)), kind: ShapeKind::Picture { fill }, ..Default::default() }; std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh]; @@ -978,7 +978,7 @@ fn a_background_is_laid_over_white() { let mut png = std::io::Cursor::new(Vec::new()); image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(4, 4, image::Rgb([0, 0, 200]))).write_to(&mut png, image::ImageFormat::Png).unwrap(); let mut p = deck_with(vec![]); - let media = p.add_media("bg.png", "image/png", png.into_inner()); + let media = p.add_media("bg.png", "image/png", png.into_inner()).expect("media"); let pf = PictureFill { media, mode: PictureMode::Stretch { fill_rect: [0.5, 0.5, 0.0, 0.0] }, ..Default::default() }; std::sync::Arc::make_mut(&mut p.slides[0]).background = Some(deckcraft_model::Background::Fill { fill: Fill::Picture(pf) }); let img = render_slide(&p, 0, &RenderOpts { scale: 0.5, ..Default::default() }); @@ -1474,7 +1474,7 @@ fn half_red_wmf() -> Vec { #[test] fn metafile_pictures_draw_their_vectors() { let mut p = Presentation::default(); - let media = p.add_media("image1.wmf", "image/x-wmf", half_red_wmf()); + let media = p.add_media("image1.wmf", "image/x-wmf", half_red_wmf()).expect("media"); let fill = deckcraft_model::style::PictureFill { media, ..Default::default() }; let sh = Shape { id: ShapeId(600), xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 200.0)), kind: ShapeKind::Picture { fill }, ..Default::default() }; std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh]; @@ -1499,7 +1499,7 @@ fn broken_metafiles_draw_nothing_and_never_panic() { cases.push(huge); for bytes in cases { let mut p = Presentation::default(); - let media = p.add_media("image1.emf", "image/x-emf", bytes); + let media = p.add_media("image1.emf", "image/x-emf", bytes).expect("media"); let fill = deckcraft_model::style::PictureFill { media, ..Default::default() }; let sh = Shape { id: ShapeId(600), xfrm: Some(Xfrm::new(0.0, 0.0, 400.0, 200.0)), kind: ShapeKind::Picture { fill }, ..Default::default() }; std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh]; @@ -1520,7 +1520,7 @@ fn an_object_deckcraft_doesnt_model_shows_its_preview_picture() { }; for with_preview in [true, false] { let mut p = Presentation::default(); - let media = p.add_media("image1.png", "image/png", png.clone()); + let media = p.add_media("image1.png", "image/png", png.clone()).expect("media"); let sh = Shape { id: ShapeId(600), xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 200.0)), @@ -1892,7 +1892,7 @@ fn a_picture_casts_its_shadow_from_its_pixels() { }; for as_fill in [false, true] { let mut p = Presentation::default(); - let media = p.add_media("logo.png", "image/png", png.clone()); + let media = p.add_media("logo.png", "image/png", png.clone()).expect("media"); let pf = deckcraft_model::style::PictureFill { media, ..Default::default() }; let mut sh = Shape { id: ShapeId(600), xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 200.0)), ..Default::default() }; if as_fill { @@ -2489,7 +2489,7 @@ fn render_an_emf_file_to_look_at() { let (Ok(src), Ok(out)) = (std::env::var("DECKCRAFT_EMF_IN"), std::env::var("DECKCRAFT_EMF_PNG")) else { return }; let Ok(bytes) = std::fs::read(&src) else { return }; let mut p = Presentation::default(); - let media = p.add_media("image1.emf", "image/x-emf", bytes); + let media = p.add_media("image1.emf", "image/x-emf", bytes).expect("media"); let fill = deckcraft_model::style::PictureFill { media, ..Default::default() }; let sh = Shape { id: ShapeId(600), xfrm: Some(Xfrm::new(40.0, 40.0, 840.0, 400.0)), kind: ShapeKind::Picture { fill }, ..Default::default() }; std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh]; diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index 672e7dcd..77fe87ac 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -265,7 +265,7 @@ fn png(w: u32, h: u32, rgb: [u8; 3]) -> Vec { fn picture_deck(line: Option) -> Presentation { let mut p = Presentation::default(); - let media = p.add_media("image1.png", "image/png", png(8, 8, [0, 0, 255])); + let media = p.add_media("image1.png", "image/png", png(8, 8, [0, 0, 255])).expect("media"); let sh = Shape { id: ShapeId(15), xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)), @@ -399,7 +399,7 @@ fn half_red_half_blue() -> Vec { fn tiled(sx: f64, flip: &str, align: &str) -> Image { use deckcraft_model::style::{PictureFill, PictureMode}; let mut p = Presentation::default(); - let media = p.add_media("t.png", "image/png", half_red_half_blue()); + let media = p.add_media("t.png", "image/png", half_red_half_blue()).expect("media"); let mode = PictureMode::Tile { tx: 0.0, ty: 0.0, sx, sy: 10.0, flip: flip.into(), align: align.into() }; let sh = Shape { id: ShapeId(21), @@ -606,7 +606,7 @@ fn a_large_pictures_shadow_is_whole() { let mut counts = vec![]; for side in [8u32, 400] { let mut p = Presentation::default(); - let media = p.add_media("image1.png", "image/png", png(side, side * 3 / 4, [0, 0, 255])); + let media = p.add_media("image1.png", "image/png", png(side, side * 3 / 4, [0, 0, 255])).expect("media"); let mut fx = offset_shadow(); if let Some(o) = &mut fx.outer_shadow { o.blur = 8.0; @@ -690,7 +690,7 @@ fn an_upright_picture_fill_stays_upright_in_a_turned_shape() { let blue = Rgba::rgb(0, 0, 255); for upright in [false, true] { let mut p = Presentation::default(); - let media = p.add_media("t.png", "image/png", half_red_half_blue()); + let media = p.add_media("t.png", "image/png", half_red_half_blue()).expect("media"); // Half red (left) half blue picture in a 200 × 200 square turned a quarter (and so // covering the same square). let sh = Shape { diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 452e46b5..d647a3e7 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -7,7 +7,7 @@ use deckcraft_model::{LayoutType, Presentation, Xfrm}; fn setup(text: &str, w: f64, h: f64) -> (Presentation, Shape) { let mut p = Presentation::default(); let lay = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap(); - let mut s = defaults::new_slide(&mut p, lay); + let mut s = defaults::new_slide(&mut p, lay).expect("slide"); let mut body = s.shapes.remove(1); body.xfrm = Some(Xfrm::new(0.0, 0.0, w, h)); body.text = Some(TextBody::from_text(text)); diff --git a/crates/ui-egui/src/media.rs b/crates/ui-egui/src/media.rs index 4d65fd40..7fd8fda7 100644 --- a/crates/ui-egui/src/media.rs +++ b/crates/ui-egui/src/media.rs @@ -536,7 +536,7 @@ mod tests { fn deck(shape: u32, bytes: Vec) -> (Presentation, ShapeId, MediaClip) { let mut doc = Presentation::default(); - let media = doc.add_media("clip.wav", "audio/wav", bytes); + let media = doc.add_media("clip.wav", "audio/wav", bytes).expect("media"); let clip = MediaClip { media, volume: 1.0, ..Default::default() }; let id = ShapeId(shape); let slide = Arc::make_mut(&mut doc.slides[0]); @@ -593,7 +593,7 @@ mod tests { fn gif_clocks_do_not_carry_across_documents() { let gif = |shape: u32| { let mut doc = Presentation::default(); - let media = doc.add_media("a.gif", "image/gif", b"GIF89a-not-animated".to_vec()); + let media = doc.add_media("a.gif", "image/gif", b"GIF89a-not-animated".to_vec()).expect("media"); let id = ShapeId(shape); let slide = Arc::make_mut(&mut doc.slides[0]); slide.shapes.push(Shape {