Export: PNG with a transparent background (#784)

The renderer lays every slide background over white (as PowerPoint does),
so a PNG export was always opaque. A new transparentBackground option leaves
the slide background out (both the white underlay and the background fill),
so only what is on the slide is drawn over transparency. Shapes filled with
the background (useBgFill) still show it.

- render: RenderOpts::transparent_background
- engine: file.export {transparentBackground} (PNG only; other formats
  refuse it and write nothing), file.render {transparentBackground}
- UI: Transparent background checkbox in the Export dialog (PNG) and in
  File > Export > Change File Type > PNG; the web download honours it
- CLI: render --transparent
This commit is contained in:
Gert Pellin
2026-10-16 16:30:52 +02:00
parent 1dc69720db
commit 39e7fb1d4f
11 changed files with 237 additions and 17 deletions
+9
View File
@@ -238,6 +238,10 @@ pub struct RenderOpts<'a> {
pub date_text: Option<String>,
/// The local date and time date fields show (`None`: the system clock).
pub now: Option<DateTime>,
/// Leave the slide background out (neither the white underlay nor the background fill), so
/// only what is on the slide is drawn over transparency: PNG export with a transparent
/// background. Shapes filled with the background (`useBgFill`) still show it.
pub transparent_background: bool,
}
impl Default for RenderOpts<'_> {
@@ -257,6 +261,7 @@ impl Default for RenderOpts<'_> {
clear: None,
date_text: None,
now: None,
transparent_background: false,
}
}
}
@@ -794,6 +799,10 @@ impl Renderer {
fn background(&mut self, ctx: &mut RenderContext, f: &Frame, rctx: &Ctx, slide: Option<&Slide>, view: Affine) {
let (fill, ph) = resolve::background(rctx, slide);
if f.opts.transparent_background {
self.bg = Some((fill, ph, view));
return;
}
let r = Rect::new(0.0, 0.0, f.pres.slide_size.width, f.pres.slide_size.height);
ctx.set_transform(view);
let path = r.to_path(0.1);
+27
View File
@@ -992,6 +992,33 @@ fn a_background_is_laid_over_white() {
assert!(g[3] == 255 && (g[0] as i32 - 127).abs() <= 3, "half-transparent black over white is grey: {g:?}");
}
#[test]
fn a_transparent_background_leaves_the_slide_background_out() {
// #784: PNG export with a transparent background draws only what is on the slide. The
// background (a solid colour here) and its white underlay are left out; the shapes, and a
// shape filled with the background (`useBgFill`), still draw.
use deckcraft_model::style::Fill;
let blue = ColorRef::rgb(deckcraft_color::Rgba::rgb(0, 0, 200));
let red = Shape {
id: ShapeId(784),
xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)),
fill: Some(Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(220, 0, 0)))),
..Default::default()
};
let bg_filled = Shape { id: ShapeId(785), xfrm: Some(Xfrm::new(500.0, 100.0, 200.0, 100.0)), fill: Some(Fill::Background), ..Default::default() };
let mut p = deck_with(vec![red, bg_filled]);
std::sync::Arc::make_mut(&mut p.slides[0]).background = Some(deckcraft_model::Background::Fill { fill: Fill::solid(blue) });
let opaque = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() });
assert_eq!(opaque.pixel(20, 20), [0, 0, 200, 255], "without the option the background draws");
let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, transparent_background: true, ..Default::default() });
assert_eq!(img.pixel(20, 20), [0, 0, 0, 0], "no background, no white underlay");
assert_eq!(img.pixel(200, 150), [220, 0, 0, 255], "the slide's shapes still draw");
assert_eq!(img.pixel(600, 150), [0, 0, 200, 255], "a background-filled shape still shows the background");
// The default theme background (a style reference) is left out too.
let img = render_slide(&deck_with(vec![]), 0, &RenderOpts { scale: 0.25, transparent_background: true, ..Default::default() });
assert!(img.pixels.as_chunks::<4>().0.iter().all(|px| px[3] == 0), "a blank slide is fully transparent");
}
#[test]
fn colour_glyphs_paint_their_layers() {
// A COLRv0 test font (crates/render/tests/fixtures): U+E000 is a red square with a blue
+1
View File
@@ -113,6 +113,7 @@ fn copy_opts<'a>(o: &RenderOpts<'a>) -> RenderOpts<'a> {
clear: o.clear,
date_text: o.date_text.clone(),
now: o.now,
transparent_background: o.transparent_background,
}
}