Merge PR #438 from albayazid: Model: an exhausted id space refuses growth instead of duplicating ids (#407)

Conflicts resolved onto main's newer shape, and the PR's policy applied to
every alloc_id/add_media/new_slide call site main has grown since: commands
answer bad(cmd, NO_ID_SPACE) (slide.reuse, insert.zoom, SmartArt, photo
album, review.accept, handout/notes masters, master placeholders,
background/artistic/compress pictures, transition sound, ink to shape,
paste special, file.new templates); reid/bring_media/renumber/regenerate
and compare::adopt/accept are fallible; outline import keeps what fit.
Tests use expect() on the now-Option results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-14 16:24:26 -04:00
co-authored by Claude Opus 5.5
48 changed files with 361 additions and 242 deletions
+1
View File
@@ -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 |
+1 -1
View File
@@ -189,7 +189,7 @@ fn group(s: &mut Session, p: &Value) -> Result<Value> {
let xs: Vec<Xfrm> = members.iter().filter_map(|m| m.xfrm).collect();
let b = union_bounds(&xs).ok_or_else(|| bad("arrange.group", "nothing to group"))?;
let gx = Xfrm::new(b.x0, b.y0, b.width(), b.height());
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("arrange.group", NO_ID_SPACE))?);
let n = super::current_shapes(doc, sel).map(|v| v.len()).unwrap_or(0);
let g = Shape {
id,
+4 -1
View File
@@ -173,7 +173,10 @@ fn artistic_effect(s: &mut Session, p: &Value) -> Result<Value> {
s.edit(move |doc, sel| {
let mut set: Vec<(ShapeId, MediaId, Option<MediaId>)> = Vec::new();
for (id, original, processed) in &results {
let m = processed.as_ref().map(|(name, png)| doc.add_media(name, "image/png", png.clone()));
let m = match processed {
Some((name, png)) => Some(doc.add_media(name, "image/png", png.clone()).ok_or_else(|| bad("picture.artisticEffect", NO_ID_SPACE))?),
None => None,
};
set.push((*id, *original, m));
}
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("picture.artisticEffect", "no slide"))?;
+4 -2
View File
@@ -165,8 +165,10 @@ fn remove_background(s: &mut Session, p: &Value) -> Result<Value> {
}
let n = done.len();
s.edit(|doc, sel| {
let added: Vec<(ShapeId, deckcraft_model::MediaId)> =
done.into_iter().map(|(id, name, png)| (id, doc.add_media(&name, "image/png", png))).collect();
let added: Vec<(ShapeId, deckcraft_model::MediaId)> = done
.into_iter()
.map(|(id, name, png)| Ok((id, doc.add_media(&name, "image/png", png).ok_or_else(|| bad("picture.removeBackground", NO_ID_SPACE))?)))
.collect::<Result<_>>()?;
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad("picture.removeBackground", "no slide"))?;
for (id, media) in added {
if let Some(sh) = deckcraft_model::find_shape_mut(shapes, id)
+15 -10
View File
@@ -169,14 +169,19 @@ fn compress(s: &mut Session, p: &Value) -> Result<Value> {
let old = m.data.len();
pack(&m.data, crop, w, h, ppi, delete_cropped).map(|pk| (pk, old, m.name.clone()))
});
let v = packed.map(|(pk, old, name)| {
before += old;
after += pk.bytes.len();
let stem = std::path::Path::new(&name).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or(name);
let ext = if pk.content_type == "image/jpeg" { "jpg" } else { "png" };
let id = doc.add_media(&format!("{stem}.{ext}"), pk.content_type, pk.bytes);
(id, pk.crop, old)
});
let v = match packed {
Some((pk, old, name)) => {
before += old;
after += pk.bytes.len();
let stem = std::path::Path::new(&name).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or(name);
let ext = if pk.content_type == "image/jpeg" { "jpg" } else { "png" };
let id = doc
.add_media(&format!("{stem}.{ext}"), pk.content_type, pk.bytes)
.ok_or_else(|| bad("picture.compress", NO_ID_SPACE))?;
Some((id, pk.crop, old))
}
None => None,
};
cache.insert(key, v);
v
}
@@ -291,8 +296,8 @@ mod tests {
let big = png(1200, 600);
let processed = png(1100, 600);
s.edit(|doc, _| {
let original = doc.add_media("original.png", "image/png", big.clone());
let shown = doc.add_media("processed.png", "image/png", processed.clone());
let original = doc.add_media("original.png", "image/png", big.clone()).expect("media");
let shown = doc.add_media("processed.png", "image/png", processed.clone()).expect("media");
let slide = Arc::make_mut(doc.slides.first_mut().ok_or_else(|| bad("test", "no slide"))?);
let xfrm = Some(deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 36.0));
let with_effect =
+16 -11
View File
@@ -166,7 +166,7 @@ fn browse_theme(s: &mut Session, p: &Value) -> Result<Value> {
// The theme's pictures (format-scheme blip fills) get ids of this deck.
let mut map = std::collections::HashMap::new();
for m in &media {
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone());
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone()).ok_or_else(|| bad("design.browseTheme", NO_ID_SPACE))?;
map.insert(m.id.0, id.0);
}
let theme = if map.is_empty() { theme.clone() } else { remap_media_ids(&theme, &map)? };
@@ -519,7 +519,7 @@ fn background(s: &mut Session, p: &Value) -> Result<Value> {
} else if p.get("picture").is_some() {
let (name, bytes) = super::insert::media_bytes(&with_param(p, "data", p.get("picture").cloned().unwrap_or_default()), "design.background")?;
let ct = super::insert::content_type(&name, &bytes);
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("design.background", NO_ID_SPACE)))?;
let mut pf = deckcraft_model::style::PictureFill { media, ..Default::default() };
if let Some(pp) = &props {
apply_picture_props(&mut pf, pp);
@@ -853,7 +853,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
};
let mut new_ids = vec![];
for _ in 0..3 {
new_ids.push(ShapeId(doc.alloc_id()));
new_ids.push(ShapeId(doc.alloc_id().ok_or_else(|| bad("design.headerFooter", NO_ID_SPACE))?));
}
let Some(sl) = doc.slides.get_mut(i) else { continue };
let sl = Arc::make_mut(sl);
@@ -1024,7 +1024,7 @@ fn handout_view(s: &mut Session, _p: &Value) -> Result<Value> {
};
let size = doc.notes_size;
for (_, k) in HANDOUT_KINDS {
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("view.handoutMaster", NO_ID_SPACE))?);
m.shapes.push(handout_placeholder(k, size, id));
}
doc.handout_master = Some(Arc::new(m));
@@ -1044,7 +1044,11 @@ fn handout_placeholders(s: &mut Session, p: &Value) -> Result<Value> {
let size = doc.notes_size;
let mut ids = vec![];
for (key, _) in HANDOUT_KINDS {
ids.push(p.get(key).and_then(Value::as_bool).map(|_| ShapeId(doc.alloc_id())));
let id = match p.get(key).and_then(Value::as_bool) {
Some(_) => Some(ShapeId(doc.alloc_id().ok_or_else(|| bad("handout.placeholders", NO_ID_SPACE))?)),
None => None,
};
ids.push(id);
}
let m = Arc::make_mut(doc.handout_master.as_mut().ok_or_else(|| bad("handout.placeholders", "no handout master"))?);
for ((key, kind), id) in HANDOUT_KINDS.into_iter().zip(ids) {
@@ -1128,7 +1132,7 @@ fn notes_view(s: &mut Session, _p: &Value) -> Result<Value> {
});
let size = doc.notes_size;
for (_, k) in NOTES_KINDS {
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("view.notesMaster", NO_ID_SPACE))?);
m.shapes.push(notes_placeholder(k, size, id));
}
doc.notes_master = Some(Arc::new(m));
@@ -1146,7 +1150,8 @@ fn notes_view(s: &mut Session, _p: &Value) -> Result<Value> {
fn notes_placeholders(s: &mut Session, p: &Value) -> Result<Value> {
s.edit(|doc, _| {
let size = doc.notes_size;
let ids: Vec<ShapeId> = NOTES_KINDS.iter().map(|_| ShapeId(doc.alloc_id())).collect();
let ids: Vec<ShapeId> =
NOTES_KINDS.iter().map(|_| doc.alloc_id().map(ShapeId).ok_or_else(|| bad("notes.placeholders", NO_ID_SPACE))).collect::<Result<_>>()?;
let m = Arc::make_mut(doc.notes_master.as_mut().ok_or_else(|| bad("notes.placeholders", "no notes master"))?);
for ((key, kind), id) in NOTES_KINDS.into_iter().zip(ids) {
let Some(on) = p.get(key).and_then(Value::as_bool) else { continue };
@@ -1180,8 +1185,8 @@ fn insert_layout(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").unwrap_or("Custom Layout").to_string();
s.edit(|doc, sel| {
let mi = master_index(doc, sel);
let id = deckcraft_model::LayoutId(doc.alloc_id());
let tid = ShapeId(doc.alloc_id());
let id = deckcraft_model::LayoutId(doc.alloc_id().ok_or_else(|| bad("master.insertLayout", NO_ID_SPACE))?);
let tid = ShapeId(doc.alloc_id().ok_or_else(|| bad("master.insertLayout", NO_ID_SPACE))?);
let size = doc.slide_size;
let m = Arc::make_mut(doc.masters.get_mut(mi).ok_or_else(|| bad("master.insertLayout", "no master"))?);
let title = m.shapes.iter().find(|x| x.ph_type() == Some(PhType::Title)).cloned().map(|mut t| {
@@ -1273,7 +1278,7 @@ fn insert_placeholder(s: &mut Session, p: &Value) -> Result<Value> {
text: Some(TextBody::default()),
..Default::default()
};
let id = super::insert::add_shape(s, shape, true)?;
let id = super::insert::add_shape(s, "master.insertPlaceholder", shape, true)?;
Ok(json!({"id": id}))
}
@@ -1342,7 +1347,7 @@ fn placeholders(s: &mut Session, p: &Value) -> Result<Value> {
s.edit(|doc, sel| {
let mut ids = Vec::with_capacity(add.len());
for _ in 0..add.len() {
ids.push(ShapeId(doc.alloc_id()));
ids.push(ShapeId(doc.alloc_id().ok_or_else(|| bad("master.placeholders", NO_ID_SPACE))?));
}
let prototypes = match sel.target {
Target::Master { master } | Target::Layout { master, .. } => doc.masters.get(master).map(|m| m.shapes.clone()).unwrap_or_default(),
+24 -17
View File
@@ -184,22 +184,29 @@ fn cut(s: &mut Session, p: &Value) -> Result<Value> {
/// attached to a shape that was copied too follow its copy; ends attached to a shape outside the
/// copied set are detached (the connector keeps its geometry), so a copy never stays glued to
/// the original or to an unrelated shape that happens to share an id in another deck.
pub(crate) fn reid(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]) -> HashMap<ShapeId, ShapeId> {
fn rec(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape], map: &mut HashMap<ShapeId, ShapeId>, depth: usize) {
pub(crate) fn reid(cmd: &str, doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]) -> Result<HashMap<ShapeId, ShapeId>> {
fn rec(
cmd: &str,
doc: &mut deckcraft_model::Presentation,
shapes: &mut [Shape],
map: &mut HashMap<ShapeId, ShapeId>,
depth: usize,
) -> Result<()> {
if depth > 64 {
return;
return Ok(());
}
for s in shapes {
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?);
map.insert(s.id, id);
s.id = id;
if let Some(ch) = s.children_mut() {
rec(doc, ch, map, depth + 1);
rec(cmd, doc, ch, map, depth + 1)?;
}
}
Ok(())
}
let mut map = HashMap::new();
rec(doc, shapes, &mut map, 0);
rec(cmd, doc, shapes, &mut map, 0)?;
walk_mut(shapes, &mut |s| {
if let deckcraft_model::ShapeKind::Connector { start, end } = &mut s.kind {
for e in [start, end] {
@@ -207,7 +214,7 @@ pub(crate) fn reid(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]
}
}
});
map
Ok(map)
}
/// Point copied animations at the copies: targets and triggers in `map` follow it; an animation
@@ -267,19 +274,19 @@ fn remap_media(shapes: &mut [Shape], map: &HashMap<MediaId, MediaId>) {
/// Add the clipboard's media to `doc` and return the ids that changed: an id already used in
/// `doc` by other bytes (another deck) gets a fresh id, and the pasted shapes must use it.
fn bring_media(doc: &mut deckcraft_model::Presentation, media: &[deckcraft_model::MediaItem]) -> HashMap<MediaId, MediaId> {
fn bring_media(cmd: &str, doc: &mut deckcraft_model::Presentation, media: &[deckcraft_model::MediaItem]) -> Result<HashMap<MediaId, MediaId>> {
let mut map = HashMap::new();
for m in media {
match doc.media(m.id) {
None => doc.media.push(m.clone()),
Some(x) if x.data == m.data => {}
Some(_) => {
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone());
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone()).ok_or_else(|| bad(cmd, NO_ID_SPACE))?;
map.insert(m.id, id);
}
}
}
map
Ok(map)
}
fn paste(s: &mut Session, p: &Value) -> Result<Value> {
@@ -308,12 +315,12 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
if !clip.slides.is_empty() {
let at = s.doc()?.selection.slide + 1;
return s.edit(|doc, sel| {
let media = bring_media(doc, &clip.media);
let media = bring_media("edit.paste", doc, &clip.media)?;
let mut new_ids = vec![];
for (k, sl) in clip.slides.iter().enumerate() {
let mut sl = sl.clone();
sl.id = SlideId(doc.alloc_id());
let map = reid(doc, &mut sl.shapes);
sl.id = SlideId(doc.alloc_id().ok_or_else(|| bad("edit.paste", NO_ID_SPACE))?);
let map = reid("edit.paste", doc, &mut sl.shapes)?;
remap_animations(&mut sl.animations, &map);
remap_media(&mut sl.shapes, &media);
if let Some(deckcraft_model::Background::Fill { fill: deckcraft_model::Fill::Picture(pf) }) = &mut sl.background
@@ -341,9 +348,9 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
let same_slide = s.doc().ok().is_some_and(|d| clip.shapes.iter().any(|c| d.shape(c.id).is_some()));
let off = if same_slide { off } else { 0.0 };
return s.edit(|doc, sel| {
let media = bring_media(doc, &clip.media);
let media = bring_media("edit.paste", doc, &clip.media)?;
let mut shapes = clip.shapes.clone();
reid(doc, &mut shapes);
reid("edit.paste", doc, &mut shapes)?;
remap_media(&mut shapes, &media);
for sh in &mut shapes {
if let Some(x) = sh.xfrm.as_mut() {
@@ -491,7 +498,7 @@ fn paste_picture(s: &mut Session) -> Result<Value> {
}
let st = s.doc()?;
let mut doc = (*st.doc).clone();
bring_media(&mut doc, &clip.media);
bring_media("edit.pasteSpecial", &mut doc, &clip.media)?;
let mut slide = st.current_slide().cloned().ok_or_else(|| bad("edit.pasteSpecial", "no slide"))?;
let xs: Vec<deckcraft_geom::Xfrm> = clip.shapes.iter().filter_map(|x| x.xfrm).collect();
let b = super::arrange::union_bounds(&xs).ok_or_else(|| bad("edit.pasteSpecial", "nothing to draw"))?;
@@ -527,7 +534,7 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
let list = super::current_shapes(doc, sel).cloned().unwrap_or_default();
let mut copies: Vec<Shape> = list.iter().filter(|x| ids.contains(&x.id)).cloned().collect();
let boxes: Vec<_> = copies.iter().map(|c| xfrm_of(doc, sel, c)).collect();
reid(doc, &mut copies);
reid("edit.duplicate", doc, &mut copies)?;
for (c, b) in copies.iter_mut().zip(boxes) {
let mut x = b;
x.x += off;
+21 -15
View File
@@ -149,9 +149,9 @@ fn default_rect(s: &Session, w: f64, h: f64) -> Xfrm {
Xfrm::new((size.width - w) / 2.0, (size.height - h) / 2.0, w, h)
}
pub(crate) fn add_shape(s: &mut Session, mut shape: Shape, select: bool) -> Result<ShapeId> {
pub(crate) fn add_shape(s: &mut Session, cmd: &str, mut shape: Shape, select: bool) -> Result<ShapeId> {
s.edit(|doc, sel| {
shape.id = ShapeId(doc.alloc_id());
shape.id = ShapeId(doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?);
if shape.name.is_empty() {
let base = match &shape.kind {
ShapeKind::Picture { .. } => "Picture".to_string(),
@@ -222,7 +222,7 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
if let (Some(look), false) = (&s.default_look, line_like) {
super::format::apply_painted(&mut shape, look);
}
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "shape.insert", shape, true)?;
Ok(json!({"id": id}))
}
@@ -268,7 +268,7 @@ fn insert_icon(s: &mut Session, p: &Value) -> Result<Value> {
style: Some(ShapeStyle::line(SchemeSlot::Tx1)),
..Default::default()
};
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "insert.icon", shape, true)?;
Ok(json!({"id": id}))
}
@@ -290,7 +290,7 @@ fn text_box(s: &mut Session, p: &Value) -> Result<Value> {
t.body.wrap = Some(false);
}
}
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "insert.textBox", shape, true)?;
super::text::fit_text_box(s, id)?;
if text.is_empty() {
// Start typing immediately.
@@ -426,7 +426,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
}
_ => [0.0; 4],
};
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("insert.picture", NO_ID_SPACE)))?;
let shape = Shape {
xfrm: Some(rect),
kind: ShapeKind::Picture { fill: PictureFill { media, crop, ..Default::default() } },
@@ -453,7 +453,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
})?;
return Ok(json!({"id": ph_id}));
}
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "insert.picture", shape, true)?;
Ok(json!({"id": id}))
}
@@ -544,7 +544,10 @@ fn commit_album(s: &mut Session, items: Vec<AlbumItem>, opts: AlbumOpts, replace
let result = (|| {
let album_id = match replace.as_ref().map(|(id, _)| *id) {
Some(id) if id != 0 => id,
_ => s.edit(|doc, _| Ok(doc.alloc_id()))?,
_ => {
let cmd = if replace.is_some() { "insert.photoAlbumEdit" } else { "insert.photoAlbum" };
s.edit(|doc, _| doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE)))?
}
};
let at = if let Some((_, ids)) = &replace {
let st = s.doc()?;
@@ -1190,7 +1193,7 @@ fn table(s: &mut Session, p: &Value) -> Result<Value> {
}
}
}
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Table(t), ..Default::default() }, true)?;
let id = add_shape(s, "insert.table", Shape { xfrm: Some(rect), kind: ShapeKind::Table(t), ..Default::default() }, true)?;
Ok(json!({"id": id}))
}
@@ -1247,7 +1250,7 @@ fn chart(s: &mut Session, p: &Value) -> Result<Value> {
}
let size = s.doc()?.doc.slide_size;
let rect = rect_param(p, "rect").unwrap_or(Xfrm::new(size.width * 0.15, size.height * 0.15, size.width * 0.7, size.height * 0.7));
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Chart(Box::new(c)), ..Default::default() }, true)?;
let id = add_shape(s, "insert.chart", Shape { xfrm: Some(rect), kind: ShapeKind::Chart(Box::new(c)), ..Default::default() }, true)?;
Ok(json!({"id": id}))
}
@@ -1299,12 +1302,15 @@ fn media(s: &mut Session, p: &Value, video: bool) -> Result<Value> {
});
let data = std::sync::Arc::try_unwrap(bytes).unwrap_or_else(|a| (*a).clone());
let (media, poster) = s.edit(|doc, _| {
let media = doc.add_media(&name, ct, data);
let poster = poster_png.map(|png| doc.add_media(&format!("{name}-poster.png"), "image/png", png));
let media = doc.add_media(&name, ct, data).ok_or_else(|| bad("insert.media", NO_ID_SPACE))?;
let poster = match poster_png {
Some(png) => Some(doc.add_media(&format!("{name}-poster.png"), "image/png", png).ok_or_else(|| bad("insert.media", NO_ID_SPACE))?),
None => None,
};
Ok((media, poster))
})?;
let clip = MediaClip { media, video, poster, volume: 1.0, duration_ms, width, height, ..Default::default() };
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Media(clip), descr: name, ..Default::default() }, true)?;
let id = add_shape(s, "insert.media", Shape { xfrm: Some(rect), kind: ShapeKind::Media(clip), descr: name, ..Default::default() }, true)?;
if let Some(w) = &warning {
s.ui_requests.push(crate::UiRequest::Message { text: w.clone() });
}
@@ -1346,7 +1352,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result<Value> {
}
}
}
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "insert.wordArt", shape, true)?;
super::text::fit_text_box(s, id)?;
Ok(json!({"id": id}))
}
@@ -1676,7 +1682,7 @@ fn action_button(s: &mut Session, p: &Value) -> Result<Value> {
}),
..Default::default()
};
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "insert.actionButton", shape, true)?;
Ok(json!({"id": id}))
}
+1 -1
View File
@@ -139,7 +139,7 @@ fn poster_frame(s: &mut Session, p: &Value) -> Result<Value> {
let data = s.doc()?.doc.media(m.media).map(|i| i.data.clone()).ok_or_else(|| EngineError::Other("the media data is missing".into()))?;
let png = poster_png(&data, ms).ok_or_else(|| EngineError::Other("no frame could be decoded there".into()))?;
let name = format!("poster{id}.png");
let poster = s.edit(|doc, _| Ok(doc.add_media(&name, "image/png", png)))?;
let poster = s.edit(|doc, _| doc.add_media(&name, "image/png", png).ok_or_else(|| bad("media.posterFrame", NO_ID_SPACE)))?;
edit_shapes(s, &json!({"id": id}), "media.posterFrame", |sh| {
if let ShapeKind::Media(m) = &mut sh.kind {
m.poster = Some(poster);
+1 -1
View File
@@ -237,7 +237,7 @@ fn merge(s: &mut Session, p: &Value) -> Result<Value> {
made.push(sh);
}
for sh in &mut made {
sh.id = ShapeId(doc.alloc_id());
sh.id = ShapeId(doc.alloc_id().ok_or_else(|| bad("shape.merge", NO_ID_SPACE))?);
new_ids.push(sh.id);
}
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.merge", "no slide"))?;
+4
View File
@@ -293,6 +293,10 @@ pub fn parse_params(doc: &str) -> std::result::Result<Vec<Param<'_>>, String> {
// ---------- param helpers ----------
/// The deck has used every object id (a hostile file can arrive that way): it cannot grow, and
/// growing would duplicate ids (#407).
pub(crate) const NO_ID_SPACE: &str = "the presentation has used every object id and can hold no more";
pub(crate) fn bad(cmd: &str, msg: impl Into<String>) -> EngineError {
EngineError::BadParams { cmd: cmd.into(), msg: msg.into() }
}
+21 -17
View File
@@ -61,12 +61,12 @@ fn remap_connectors(shapes: &mut [deckcraft_model::Shape], map: &HashMap<ShapeId
}
/// Fresh shape ids for `shapes`, and the old → new map.
fn renumber(doc: &mut Presentation, shapes: &mut [deckcraft_model::Shape]) -> HashMap<ShapeId, ShapeId> {
fn renumber(doc: &mut Presentation, shapes: &mut [deckcraft_model::Shape]) -> Result<HashMap<ShapeId, ShapeId>> {
let old = shape_ids(shapes);
super::edit::reid(doc, shapes);
super::edit::reid("slide.reuse", doc, shapes)?;
let map: HashMap<ShapeId, ShapeId> = old.into_iter().zip(shape_ids(shapes)).collect();
remap_connectors(shapes, &map, 0);
map
Ok(map)
}
/// The destination layout a reused slide moves to: same name, else same kind, else the first.
@@ -120,16 +120,16 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
masters_done.push(m.id);
let mut used = vec![];
media_refs::visit::master(m, &mut |id: &MediaId| used.push(*id));
copy_media(doc, &src, &used, &mut media_map);
copy_media(doc, &src, &used, &mut media_map)?;
let mut m = m.clone();
media_refs::visit_mut::master(&mut m, &mut remap(&media_map));
m.id = MasterId(doc.alloc_id());
renumber(doc, &mut m.shapes);
m.id = MasterId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
renumber(doc, &mut m.shapes)?;
for l in &mut m.layouts {
let new = LayoutId(doc.alloc_id());
let new = LayoutId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
layout_map.insert(l.id, new);
l.id = new;
renumber(doc, &mut l.shapes);
renumber(doc, &mut l.shapes)?;
}
doc.masters.push(Arc::new(m));
}
@@ -137,11 +137,11 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
let mut new_ids = vec![];
for (k, &i) in picked.iter().enumerate() {
let Some(from) = src.slides.get(i) else { continue };
copy_media(doc, &src, &media_refs::slide_media(from), &mut media_map);
copy_media(doc, &src, &media_refs::slide_media(from), &mut media_map)?;
let mut sl: Slide = from.as_ref().clone();
media_refs::visit_mut::slide(&mut sl, &mut remap(&media_map));
sl.id = SlideId(doc.alloc_id());
let map = renumber(doc, &mut sl.shapes);
sl.id = SlideId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
let map = renumber(doc, &mut sl.shapes)?;
for a in &mut sl.animations {
a.shape = map.get(&a.shape).copied().unwrap_or(a.shape);
a.trigger = a.trigger.map(|t| map.get(&t).copied().unwrap_or(t));
@@ -167,7 +167,7 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
/// Copy the source media `used` into `doc` under fresh ids, once each (identical bytes already in
/// `doc` are shared; a linked item keeps its link).
fn copy_media(doc: &mut Presentation, src: &Presentation, used: &[MediaId], map: &mut HashMap<MediaId, MediaId>) {
fn copy_media(doc: &mut Presentation, src: &Presentation, used: &[MediaId], map: &mut HashMap<MediaId, MediaId>) -> Result<()> {
for &id in used {
if map.contains_key(&id) {
continue;
@@ -175,15 +175,16 @@ fn copy_media(doc: &mut Presentation, src: &Presentation, used: &[MediaId], map:
let Some(m) = src.media(id) else { continue };
let new = if m.link.is_some() {
let mut item = m.clone();
item.id = MediaId(doc.alloc_id());
item.id = MediaId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
let new = item.id;
doc.media.push(item);
new
} else {
doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone())
doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone()).ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?
};
map.insert(id, new);
}
Ok(())
}
#[cfg(test)]
@@ -201,8 +202,11 @@ mod tests {
fn reused_slides_bring_every_media_reference() {
let mut src = defaults::new_presentation(None);
let blobs: Vec<Vec<u8>> = (0..8u8).map(|i| vec![0xD0, i, i, i]).collect();
let ids: Vec<MediaId> =
blobs.iter().enumerate().map(|(i, b)| src.add_media(&format!("m{i}.bin"), "application/octet-stream", b.clone())).collect();
let ids: Vec<MediaId> = blobs
.iter()
.enumerate()
.map(|(i, b)| src.add_media(&format!("m{i}.bin"), "application/octet-stream", b.clone()).expect("media"))
.collect();
let id = |i: usize| ids.get(i).copied().unwrap_or_default();
let mut text = TextBody::from_text("bullet");
if let Some(p) = text.paragraphs.first_mut() {
@@ -246,7 +250,7 @@ mod tests {
let mut s = Session::with_new();
let own = vec![1u8, 2, 3];
let own_id = s.edit(|doc, _| Ok(doc.add_media("own.bin", "application/octet-stream", own.clone()))).expect("own media");
let own_id = s.edit(|doc, _| Ok(doc.add_media("own.bin", "application/octet-stream", own.clone()).expect("media"))).expect("own media");
s.execute("slide.reuse", &json!({"data": super::super::base64_encode(&bytes), "name": "src.deckcraft", "slides": [0]})).expect("reuse");
let doc = &s.doc().expect("doc").doc;
assert_eq!(doc.media(own_id).map(|m| m.data.as_slice() == own.as_slice()), Some(true), "own media untouched");
+1 -1
View File
@@ -684,7 +684,7 @@ fn accept(s: &mut Session, p: &Value) -> Result<Value> {
changes.into_iter().nth(index.unwrap_or(0))
};
let Some(c) = c else { break };
let Some(at) = crate::compare::accept(doc, &rev, &c) else { break };
let Some(at) = crate::compare::accept(doc, &rev, &c)? else { break };
accepted += 1;
sel.slide = at;
sel.shapes.clear();
+3 -3
View File
@@ -538,7 +538,7 @@ fn fill(s: &mut Session, p: &Value) -> Result<Value> {
} else if p.get("picture").is_some() {
let (name, bytes) = super::insert::media_bytes(&with_param(p, "data", p.get("picture").cloned().unwrap_or_default()), "shape.fill")?;
let ct = super::insert::content_type(&name, &bytes);
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("shape.fill", NO_ID_SPACE)))?;
Some(Some(Fill::Picture(PictureFill { media, ..Default::default() })))
} else {
None
@@ -1096,7 +1096,7 @@ fn picture_reset(s: &mut Session, p: &Value) -> Result<Value> {
fn picture_change(s: &mut Session, p: &Value) -> Result<Value> {
let (name, bytes) = super::insert::media_bytes(p, "picture.change")?;
let ct = super::insert::content_type(&name, &bytes);
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("picture.change", NO_ID_SPACE)))?;
edit_shapes(s, p, "picture.change", |sh| {
if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
fill.media = media;
@@ -1454,7 +1454,7 @@ fn ink_to_shape(s: &mut Session, _p: &Value) -> Result<Value> {
list.remove(pos);
}
for (preset, xfrm, color, width) in made {
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("draw.inkToShape", NO_ID_SPACE))?);
let name = format!(
"{} {}",
match preset {
+4 -4
View File
@@ -115,7 +115,7 @@ fn new(s: &mut Session, p: &Value) -> Result<Value> {
}
}
};
let mut slide = defaults::new_slide(doc, layout);
let mut slide = defaults::new_slide(doc, layout).ok_or_else(|| bad("slide.new", NO_ID_SPACE))?;
if let Some(t) = str_param(p, "title")
&& let Some(sh) = slide.shapes.iter_mut().find(|x| x.ph_type().is_some_and(|k| k.is_title()))
{
@@ -186,9 +186,9 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
for (k, i) in indices.iter().enumerate() {
let Some(src) = doc.slides.get(*i).map(|x| x.as_ref().clone()) else { continue };
let mut copy = src;
copy.id = SlideId(doc.alloc_id());
copy.id = SlideId(doc.alloc_id().ok_or_else(|| bad("slide.duplicate", NO_ID_SPACE))?);
// Animations follow the new shape ids, triggers included.
let map = super::edit::reid(doc, &mut copy.shapes);
let map = super::edit::reid("slide.duplicate", doc, &mut copy.shapes)?;
super::edit::remap_animations(&mut copy.animations, &map);
let section = doc.section_of(src_id(doc, *i));
let at = (last + 1 + k).min(doc.slides.len());
@@ -377,7 +377,7 @@ pub fn apply_layout(doc: &mut Presentation, index: usize, layout: LayoutId) -> R
doc.layout(layout).map(|(_, l)| l.shapes.clone()).ok_or_else(|| bad("slide.layout", "no such layout"))?;
let mut fresh = vec![];
for ls in lay_shapes.iter().filter(|x| x.ph.as_ref().is_some_and(|p| !p.kind.is_footer_kind())) {
let id = deckcraft_model::ShapeId(doc.alloc_id());
let id = deckcraft_model::ShapeId(doc.alloc_id().ok_or_else(|| bad("slide.layout", NO_ID_SPACE))?);
fresh.push((id, ls.clone()));
}
let slide = doc.slides.get_mut(index).ok_or_else(|| bad("slide.layout", "no slide"))?;
+10 -10
View File
@@ -1703,12 +1703,12 @@ fn target_group(s: &Session, p: &Value, cmd: &str) -> Result<ShapeId> {
}
/// Children with fresh ids for diagram `d` inside a group whose child space is `area`.
fn regenerate(doc: &mut Presentation, d: &Diagram, area: Xfrm) -> Vec<Shape> {
fn regenerate(cmd: &str, doc: &mut Presentation, d: &Diagram, area: Xfrm) -> Result<Vec<Shape>> {
let mut v = layout(d, area);
for sh in &mut v {
sh.id = ShapeId(doc.alloc_id());
sh.id = ShapeId(doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?);
}
v
Ok(v)
}
/// Read the diagram (with typed text synced), let `f` change it, and regenerate the group.
@@ -1721,7 +1721,7 @@ fn edit_diagram<T>(s: &mut Session, p: &Value, cmd: &str, f: impl FnOnce(&mut Di
let out = f(&mut d)?;
normalize(&mut d.nodes);
d.imported = false;
let kids = regenerate(doc, &d, child);
let kids = regenerate(cmd, doc, &d, child)?;
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?;
let sh = deckcraft_model::find_shape_mut(shapes, gid).ok_or_else(|| bad(cmd, "no such shape"))?;
sh.kind = ShapeKind::Group { children: kids, child };
@@ -1747,8 +1747,8 @@ pub fn insert(s: &mut Session, kind: &str, mut nodes: Vec<DiagramNode>) -> Resul
let size = s.doc()?.doc.slide_size;
let area = Xfrm::new(size.width * 0.1, size.height * 0.28, size.width * 0.8, size.height * 0.6);
let id = s.edit(|doc, sel| {
let children = regenerate(doc, &d, area);
let id = ShapeId(doc.alloc_id());
let children = regenerate("insert.smartArt", doc, &d, area)?;
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("insert.smartArt", NO_ID_SPACE))?);
let g = Shape {
id,
name: format!("SmartArt ({kind})"),
@@ -1858,8 +1858,8 @@ fn from_text(s: &mut Session, p: &Value) -> Result<Value> {
};
let d = Diagram { kind: kind.clone(), nodes, ..Default::default() };
let gid = s.edit(|doc, sel| {
let children = regenerate(doc, &d, area);
let gid = ShapeId(doc.alloc_id());
let children = regenerate(CMD, doc, &d, area)?;
let gid = ShapeId(doc.alloc_id().ok_or_else(|| bad(CMD, NO_ID_SPACE))?);
let g = Shape {
id: gid,
name: format!("SmartArt ({kind})"),
@@ -1914,7 +1914,7 @@ fn to_text(s: &mut Session, p: &Value) -> Result<Value> {
}
body.body.wrap = Some(true);
body.body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 });
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad(CMD, NO_ID_SPACE))?);
let tb =
Shape { id, name: "Text".into(), xfrm: Some(area), text_box: true, text: Some(body), geom: Geom::preset("rect"), ..Default::default() };
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(CMD, "no slide"))?;
@@ -1989,7 +1989,7 @@ fn set_picture(s: &mut Session, p: &Value) -> Result<Value> {
let (name, bytes) = super::insert::media_bytes(p, CMD)?;
let ct = super::insert::content_type(&name, &bytes);
target_group(s, p, CMD)?;
Some(s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?)
Some(s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad(CMD, NO_ID_SPACE)))?)
};
edit_diagram(s, p, CMD, |d| {
// A sub-point's picture is its top-level node's: that's the node the placeholder shows.
+1 -1
View File
@@ -182,7 +182,7 @@ fn sound(s: &mut Session, p: &Value) -> Result<Value> {
let existing = doc.media.iter().find(|m| m.name == name && m.data.as_slice() == bytes.as_slice()).map(|m| m.id);
let media = match existing {
Some(m) => m,
None => doc.add_media(&name, ct, bytes),
None => doc.add_media(&name, ct, bytes).ok_or_else(|| bad("transition.sound", NO_ID_SPACE))?,
};
for &i in &list {
if let Some(sl) = doc.slides.get_mut(i) {
+4 -3
View File
@@ -138,9 +138,10 @@ fn zoom(s: &mut Session, p: &Value) -> Result<Value> {
let boxes = grid(targets.len(), area, aspect);
let mut shapes = vec![];
for ((t, (png, label)), b) in targets.iter().zip(pictures).zip(boxes) {
let media = doc.add_media(&format!("zoom-{}.png", t.index + 1), "image/png", png);
let media = doc.add_media(&format!("zoom-{}.png", t.index + 1), "image/png", png).ok_or_else(|| bad("insert.zoom", NO_ID_SPACE))?;
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("insert.zoom", NO_ID_SPACE))?);
shapes.push(Shape {
id: ShapeId(doc.alloc_id()),
id,
name: format!(
"{} Zoom {}",
if summary {
@@ -171,7 +172,7 @@ fn zoom(s: &mut Session, p: &Value) -> Result<Value> {
// A new Title Only slide before the first part, as PowerPoint makes it.
let layout =
defaults::layout_of_kind(doc, LayoutType::TitleOnly).or_else(|| doc.slides.get(first_target).map(|x| x.layout)).unwrap_or_default();
let mut slide = defaults::new_slide(doc, layout);
let mut slide = defaults::new_slide(doc, layout).ok_or_else(|| bad("insert.zoom", NO_ID_SPACE))?;
if let Some(sh) = slide.shapes.iter_mut().find(|x| x.ph_type().is_some_and(|k| k.is_title())) {
sh.text = Some(TextBody::from_text("Summary"));
}
+28 -22
View File
@@ -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<SlideId>) -> 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<SlideId>) -> crate::Result<Slide> {
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<ShapeId, ShapeId>,
media: &HashMap<MediaId, MediaId>,
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<usize> {
/// 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<Option<usize>> {
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))))
}
}
}
+1 -1
View File
@@ -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(),
+1 -1
View File
@@ -216,7 +216,7 @@ pub fn build(name: &str, size: Size) -> Result<Presentation> {
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()))
{
+30 -3
View File
@@ -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");
}
+2 -2
View File
@@ -1407,7 +1407,7 @@ impl Session {
let list = cmd::current_shapes(doc, sel).cloned().unwrap_or_default();
let mut copies: Vec<Shape> = list.iter().filter(|x| ids.contains(&x.id)).cloned().collect();
let boxes: Vec<Xfrm> = 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 {
+3 -3
View File
@@ -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<Shape>, animations: Vec<Animation>, 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);
+25 -8
View File
@@ -167,8 +167,9 @@ fn load_within(bytes: &[u8], budget: Budget) -> Result<(Presentation, Vec<String
Ok((p, damaged))
}
/// Make a loaded presentation safe to edit: a master exists, every slide's layout exists,
/// ids are unique, tables are rectangular.
/// Make a loaded presentation safe to edit: a master exists, every slide's layout exists, tables
/// are rectangular, and duplicate ids are re-id'd — unless the deck has used every id, in which
/// case a duplicate stays (growth is refused; see the comment in the loop).
pub fn repair(p: &mut Presentation) {
if p.masters.is_empty() {
let fresh = defaults::blank_presentation(p.slide_size, Default::default(), false);
@@ -181,7 +182,10 @@ pub fn repair(p: &mut Presentation) {
let needs_layout = p.slides.get(i).is_some_and(|s| p.layout(s.layout).is_none());
let dup = p.slides.get(i).is_some_and(|s| !seen.insert(s.id));
if needs_layout || dup {
let new_id = if dup { Some(deckcraft_model::SlideId(p.alloc_id())) } else { None };
// An exhausted id space leaves a duplicate slide as it is: the engine refuses to
// grow such a deck (edits by id stay ambiguous for the duplicate), and the duplicate
// stays visible rather than the file failing to open (#407).
let new_id = if dup { p.alloc_id().map(deckcraft_model::SlideId) } else { None };
if let Some(s) = p.slides.get_mut(i) {
let s = Arc::make_mut(s);
if needs_layout {
@@ -220,9 +224,10 @@ type OutlineSlides = Vec<(String, Vec<(u8, String)>)>;
/// 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);
+11 -6
View File
@@ -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<Slide> {
let id = SlideId(p.alloc_id()?);
let mut shapes = vec![];
let lay_shapes: Vec<Shape> = 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.
+11 -8
View File
@@ -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<u32> {
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<u8>) -> MediaId {
pub fn add_media(&mut self, name: &str, content_type: &str, data: Vec<u8>) -> Option<MediaId> {
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<usize> {
+16 -4
View File
@@ -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);
+1 -1
View File
@@ -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() {
+5 -2
View File
@@ -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(),
+12 -8
View File
@@ -14,8 +14,9 @@ fn chart_pptx() -> Vec<u8> {
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));
+1 -1
View File
@@ -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]);
+3 -3
View File
@@ -22,7 +22,7 @@ fn png(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
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;
+2 -2
View File
@@ -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 {
+4 -4
View File
@@ -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"));
}
+8 -8
View File
@@ -23,7 +23,7 @@ fn png(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
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"));
}
+7 -7
View File
@@ -26,12 +26,12 @@ fn glb(marker: u8) -> Vec<u8> {
}
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<u8>, Vec<u8>, Vec<u8>) {
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);
+2 -2
View File
@@ -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 {
+5 -5
View File
@@ -64,7 +64,7 @@ fn effects_and_links() -> Vec<u8> {
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
}
+8 -8
View File
@@ -36,8 +36,8 @@ fn png() -> Vec<u8> {
/// A deck with one shape filled with a stretched picture, exported.
fn deck() -> Vec<u8> {
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);
+3 -3
View File
@@ -15,14 +15,14 @@ fn png() -> Vec<u8> {
}
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 },
+1 -1
View File
@@ -34,7 +34,7 @@ fn rezip(es: &[(String, Vec<u8>)]) -> Vec<u8> {
/// 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!("<m:oMathPara {NS_M}><m:oMath><m:r><m:t>x=1</m:t></m:r></m:oMath></m:oMathPara>");
let eq = Shape {
id,
+20 -16
View File
@@ -27,12 +27,12 @@ fn png(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
}
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<u32>)]) -> (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#"<dgm:dataModel xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><dgm:ptLst><dgm:pt modelId="1"><dgm:t><a:p><a:r><a:t>Manager</a:t></a:r></a:p></dgm:t></dgm:pt></dgm:ptLst><dgm:extLst><a:ext uri="http://schemas.microsoft.com/office/drawing/2008/diagram"><dsp:dataModelExt xmlns:dsp="http://schemas.microsoft.com/office/drawing/2008/diagram" relId="rId6"/></a:ext></dgm:extLst></dgm:dataModel>"#.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#"<dgm:{kind} xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram"/>"#).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#"<p:graphicFrame xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:nvGraphicFramePr><p:cNvPr id="4" name="Diagram 3"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr><p:xfrm><a:off x="0" y="0"/><a:ext cx="914400" cy="914400"/></p:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/diagram"><dgm:relIds xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" r:dm="rId2" r:lo="rId3" r:qs="rId4" r:cs="rId5"/></a:graphicData></a:graphic></p:graphicFrame>"#;
@@ -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#"<p:graphicFrame xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:nvGraphicFramePr><p:cNvPr id="4" name="Object 3"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr><p:xfrm><a:off x="0" y="0"/><a:ext cx="914400" cy="914400"/></p:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/presentationml/2006/ole"><p:oleObj spid="_x0000_s1026" name="Slide" r:id="rId4" progId="PowerPoint.Slide.8"><p:embed/></p:oleObj></a:graphicData></a:graphic></p:graphicFrame>"#;
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");
+1 -1
View File
@@ -20,7 +20,7 @@ fn style_xml() -> String {
/// An exported deck with a 3x2 table using the probe style.
fn table_deck() -> Vec<u8> {
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();
+7 -7
View File
@@ -656,7 +656,7 @@ fn two_frame_gif() -> Vec<u8> {
#[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<u8> {
#[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];
+4 -4
View File
@@ -265,7 +265,7 @@ fn png(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
fn picture_deck(line: Option<Line>) -> 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<u8> {
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 {
+1 -1
View File
@@ -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));
+2 -2
View File
@@ -536,7 +536,7 @@ mod tests {
fn deck(shape: u32, bytes: Vec<u8>) -> (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 {