diff --git a/Cargo.lock b/Cargo.lock index 40f895f5..6b735c5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1112,7 +1112,7 @@ dependencies = [ [[package]] name = "designcraft-pptx" -version = "0.5.0" +version = "0.8.0" dependencies = [ "designcraft-color", "designcraft-compose", @@ -1121,7 +1121,7 @@ dependencies = [ "designcraft-geom", "designcraft-images", "image", - "quick-xml 0.38.4", + "quick-xml", "skrifa 0.47.0", "thiserror 2.0.21", "zip", diff --git a/crates/engine/src/cmd/export.rs b/crates/engine/src/cmd/export.rs index 22cd2b44..f10f3037 100644 --- a/crates/engine/src/cmd/export.rs +++ b/crates/engine/src/cmd/export.rs @@ -102,7 +102,7 @@ fn export_pptx(s: &mut Session, p: &Value) -> Result { match str_param(p, "path") { Some(path) => { #[cfg(not(target_arch = "wasm32"))] - std::fs::write(path, &r.bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?; + crate::io::write_file(path, &r.bytes).map_err(EngineError::Other)?; Ok(json!({"path": path, "bytes": r.bytes.len(), "slides": r.slides, "warnings": r.warnings})) } None => Ok(json!({"base64": super::file::base64_encode(&r.bytes), "bytes": r.bytes.len(), "slides": r.slides, "warnings": r.warnings})), diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs index fc610ff3..1cccc7fd 100644 --- a/crates/engine/src/cmd/style.rs +++ b/crates/engine/src/cmd/style.rs @@ -539,12 +539,16 @@ fn rename_style(d: &mut designcraft_doc::Document, para: bool, from: &str, to: & /// instead removes the entry, after its former uses point at the replacement). fn redirect_style_refs(d: &mut designcraft_doc::Document, para: bool, from: &str, to: &str) { let st = d.styles_mut(); + // A style is never based on itself: when `to` was based on `from`, it takes `from`'s parent. + let parent_of = |based_on: Option<&String>| based_on.filter(|b| b.as_str() != to).cloned(); if para { + let from_parent = parent_of(st.paragraph.iter().find(|x| x.name == from).and_then(|x| x.based_on.as_ref())); for s in &mut st.paragraph { - for r in [&mut s.based_on, &mut s.next_style] { - if r.as_deref() == Some(from) { - *r = Some(to.to_string()); - } + if s.based_on.as_deref() == Some(from) { + s.based_on = if s.name == to { from_parent.clone() } else { Some(to.to_string()) }; + } + if s.next_style.as_deref() == Some(from) { + s.next_style = Some(to.to_string()); } } for o in &mut st.object { @@ -553,9 +557,10 @@ fn redirect_style_refs(d: &mut designcraft_doc::Document, para: bool, from: &str } } } else { + let from_parent = parent_of(st.character.iter().find(|x| x.name == from).and_then(|x| x.based_on.as_ref())); for s in &mut st.character { if s.based_on.as_deref() == Some(from) { - s.based_on = Some(to.to_string()); + s.based_on = if s.name == to { from_parent.clone() } else { Some(to.to_string()) }; } } for s in &mut st.paragraph { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 1126b74c..616a8639 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -701,6 +701,23 @@ fn paragraph_style_delete_rejects_missing_unknown_and_self_replacement() { assert!(s.execute("style.paragraph.delete", &json!({"name": name, "replaceWith": name})).is_err(), "replaceWith can't be the deleted style"); } +#[test] +fn deleting_a_style_never_leaves_its_replacement_based_on_itself() { + let mut s = session(); + let para = |s: &Session, n: &str| s.doc().unwrap().doc.styles.para(n).unwrap().based_on.clone(); + s.execute("style.paragraph.create", &json!({"name": "Head"})).unwrap(); + s.execute("style.paragraph.create", &json!({"name": "Sub", "basedOn": "Head"})).unwrap(); + s.execute("style.paragraph.create", &json!({"name": "Minor", "basedOn": "Sub"})).unwrap(); + // Minor is based on Sub; deleting Sub in favour of Minor gives Minor Sub's parent. + s.execute("style.paragraph.delete", &json!({"name": "Sub", "replaceWith": "Minor"})).unwrap(); + assert_eq!(para(&s, "Minor").as_deref(), Some("Head")); + s.execute("style.character.create", &json!({"name": "Strong"})).unwrap(); + s.execute("style.character.create", &json!({"name": "Stronger", "basedOn": "Strong"})).unwrap(); + s.execute("style.character.delete", &json!({"name": "Strong", "replaceWith": "Stronger"})).unwrap(); + let based = s.doc().unwrap().doc.styles.character.iter().find(|c| c.name == "Stronger").unwrap().based_on.clone(); + assert_ne!(based.as_deref(), Some("Stronger")); +} + #[test] fn character_style_delete_reassigns_every_kind_of_reference() { let mut s = session(); diff --git a/crates/pptx/src/slide.rs b/crates/pptx/src/slide.rs index f4cf4386..03fd0e8b 100644 --- a/crates/pptx/src/slide.rs +++ b/crates/pptx/src/slide.rs @@ -208,7 +208,8 @@ pub struct Exporter<'a> { pub to_slide: Affine, /// The page's bleed area in spread space (items outside it are left out). clip: Rect, - pub page_name: Option, + /// The page parent items are composed for (page numbers, running heads). + pub page: Option, } impl<'a> Exporter<'a> { @@ -227,7 +228,7 @@ impl<'a> Exporter<'a> { rel_of: HashMap::new(), to_slide: Affine::IDENTITY, clip: Rect::ZERO, - page_name: None, + page: None, } } @@ -276,9 +277,9 @@ impl<'a> Exporter<'a> { continue; } // Parent items compose per page (page numbers, running heads). - self.page_name = Some(page_name.clone()); + self.page = Some(abs); self.item(it, Affine::translate((dx, 0.0)), 1.0); - self.page_name = None; + self.page = None; } } for it in &sp.items { diff --git a/crates/pptx/src/text.rs b/crates/pptx/src/text.rs index 96671771..b15cbe71 100644 --- a/crates/pptx/src/text.rs +++ b/crates/pptx/src/text.rs @@ -92,7 +92,7 @@ pub(crate) struct Segment { pub fn frame(ex: &mut Exporter, it: &Item, tfr: &TextFrame, xf: Affine, opacity: f32) { let doc = ex.doc; - let cs = ex.cache.get(doc, tfr.story, ex.page_name.as_deref()); + let cs = ex.cache.get(doc, tfr.story, ex.page); let Some(ft) = cs.frame(it.id) else { ex.shape(it, xf, opacity, None); return; diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 350ea526..b3379c6d 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -1830,4 +1830,5 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Convert to Flex Layout", "მოქნილ განლაგებად გარდაქმნა"), ("Remove Flex Layout", "მოქნილი განლაგების წაშლა"), ("Show Flex Controls", "მოქნილი მართვის ელემენტების ჩვენება"), + ("Export PowerPoint…", "PowerPoint-ად ექსპორტი…"), ];