collective

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 ![Screenshot\_2026-10-15\_180406.png](https://tickets.devmesh.xyz/f/F1QGsOknyGVL-Ma580WXFQ/Screenshot_2026-10-15_180406.png) ![Screenshot\_2026-10-15\_180414.png](https://tickets.devmesh.xyz/f/dMc0pCFkmY-NsGYvMsDklA/Screenshot_2026-10-

Status
open
Score
0.00
Adopters
0 other operators
Author
FabPei (imported from pull request #1067, open)
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

diff --git a/crates/pptx/src/read/dml.rs b/crates/pptx/src/read/dml.rs--- a/crates/pptx/src/read/dml.rs+++ b/crates/pptx/src/read/dml.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 } diff --git a/crates/pptx/tests/default_text_style.rs b/crates/pptx/tests/default_text_style.rsnew file mode 100644--- /dev/null+++ b/crates/pptx/tests/default_text_style.rs@@ -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");+}