Browse / github.com/storytold/deckcraft / patch
PPTX: text keeps the colours of the deck's default text style (#810)
[Bug Report] [DeckCraft] Custom text colors reset to black when opening .pptx file ### Description When I open a .pptx file in DeckCraft, all text colors (white, yellow, light blue) disappear and reset to black, making the text unreadable on a dark background. However, when I open the exact same file in Google Slides, the colors display properly. ### Operating system and version Windows 11 26H2 <!-- ticket-relay:files --> ### Attachments  
- Base
fe510bdac68118f2c9a714e69d0ff572e3f9a0c7- Size
- +96 −0 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
875a1a5353c4109bb7da4e8d
How it went
No other operator has reported using it yet.
Diff
raw · crates/pptx/src/read/dml.rs crates/pptx/tests/default_text_style.rs
@@ -545,8 +545,15 @@ pub fn rpr(imp: &mut Imp, part: &Part, r: &El) -> RunProps { /// `a:lstStyle`, `p:titleStyle`, `a:defaultTextStyle`… → list style. pub fn list_style(imp: &mut Imp, part: &Part, ls: &El) -> ListStyle { let mut out = ListStyle::default();+ let mut def = None; for e in ls.elements() { let n = e.local();+ if n == "defPPr" {+ // Defaults for every level (`a:defPPr`, as `p:defaultTextStyle` carries them).+ let (_, para, run) = ppr(imp, part, e);+ def = Some(LevelStyle { para, run: run.unwrap_or_default() });+ continue;+ } let Some(l) = n.strip_prefix("lvl").and_then(|r| r.strip_suffix("pPr")).and_then(|d| d.parse::<u8>().ok()) else { continue }; if !(1..=9).contains(&l) { continue;@@ -554,6 +561,19 @@ pub fn list_style(imp: &mut Imp, part: &Part, ls: &El) -> ListStyle { let (_, para, run) = ppr(imp, part, e); out.set(l - 1, LevelStyle { para, run: run.unwrap_or_default() }); }+ // A level's own `a:lvlNpPr` wins over the defaults; the defaults reach levels without one.+ // Dropping them loses the colours of text that takes its formatting from `p:defaultTextStyle`+ // (the text draws in the plain default colour instead; #810).+ if let Some(d) = &def {+ for l in 0..9u8 {+ if let Some(lvl) = out.levels.get_mut(l as usize).and_then(|x| x.as_mut()) {+ lvl.para.inherit(&d.para);+ lvl.run.inherit(&d.run);+ } else {+ out.set(l, LevelStyle { para: d.para.clone(), run: d.run.clone() });+ }+ }+ } out } new file mode 100644@@ -0,0 +1,76 @@+//! `a:defPPr` colours in `p:defaultTextStyle` as PowerPoint writes them, kept on open (#810).++use std::io::{Cursor, Read, Write};++use deckcraft_model::defaults;+use deckcraft_model::style::{ColorRef, Fill};+use deckcraft_model::text::TextBody;++/// Imports a one-slide deck whose text box runs set no colour of their own, with+/// `ppt/presentation.xml`'s `p:defaultTextStyle` replaced by `dts`, and returns every run's+/// resolved fill (what the renderer draws).+fn resolved_fills(dts: &str) -> Vec<Option<Fill>> {+ let mut p = defaults::new_presentation(None);+ let sid = p.slides[0].id;+ let new_id = deckcraft_model::ShapeId(p.alloc_id().expect("id"));+ let s = p.slide_mut(sid).expect("slide");+ // A plain text box: no placeholder and no styles of its own, so its runs take the+ // presentation's default text style.+ s.shapes.push(deckcraft_model::Shape {+ id: new_id,+ xfrm: Some(deckcraft_geom::Xfrm::new(100.0, 100.0, 400.0, 100.0)),+ text: Some(TextBody::from_text("plain")),+ ..Default::default()+ });+ let bytes = deckcraft_pptx::export(&p).expect("export");+ let mut z = zip::ZipArchive::new(Cursor::new(&bytes)).expect("zip");+ let mut out = zip::ZipWriter::new(Cursor::new(Vec::new()));+ let o = zip::write::SimpleFileOptions::default();+ for i in 0..z.len() {+ let mut f = z.by_index(i).expect("entry");+ let mut d = vec![];+ f.read_to_end(&mut d).expect("read");+ if f.name() == "ppt/presentation.xml" {+ let s = String::from_utf8(d).expect("utf8");+ let t = s.find("<p:defaultTextStyle").expect("defaultTextStyle");+ let end = "</p:defaultTextStyle>";+ let b = t + s[t..].find(end).expect("end") + end.len();+ d = format!("{}{dts}{}", &s[..t], &s[b..]).into_bytes();+ }+ out.start_file(f.name(), o).expect("start");+ out.write_all(&d).expect("write");+ }+ let bytes = out.finish().expect("finish").into_inner();+ let q = deckcraft_pptx::import(&bytes).expect("import");+ let slide = &q.slides[0];+ let ctx = deckcraft_model::resolve::Ctx::for_slide(&q, slide).expect("ctx");+ let mut fills = vec![];+ for sh in slide.shapes.iter().filter_map(|s| s.text.as_ref().map(|t| (s, t))) {+ for para in sh.1.paragraphs.iter() {+ for run in para.runs.iter() {+ fills.push(deckcraft_model::resolve::run(&ctx, sh.0, para, &run.props).fill);+ }+ }+ }+ fills+}++const NS: &str =+ r#"xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main""#;++/// The colours of text that takes its formatting from `p:defaultTextStyle` were dropped, so it+/// drew in the plain default colour instead of the colour the deck chose (#810).+#[test]+fn default_text_style_colours_are_kept() {+ let white = Some(Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::WHITE)));+ let dts = format!(+ r#"<p:defaultTextStyle {NS}><a:defPPr><a:defRPr><a:solidFill><a:srgbClr val="FFFFFF"/></a:solidFill></a:defRPr></a:defPPr></p:defaultTextStyle>"#+ );+ assert_eq!(resolved_fills(&dts), vec![white.clone()], "`a:defPPr` colours reach text without its own");++ // A level's own `a:lvlNpPr` still wins over the defaults.+ let dts = format!(+ r#"<p:defaultTextStyle {NS}><a:defPPr><a:defRPr><a:solidFill><a:srgbClr val="FFFFFF"/></a:solidFill></a:defRPr></a:defPPr><a:lvl1pPr><a:defRPr><a:solidFill><a:srgbClr val="FFFF00"/></a:solidFill></a:defRPr></a:lvl1pPr></p:defaultTextStyle>"#+ );+ assert_eq!(resolved_fills(&dts), vec![Some(Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(255, 255, 0))))], "the level entry wins");+}