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
+5 -4
View File
@@ -19,15 +19,16 @@ The shapes of one slide, with their ids, boxes and text, come from the slide.ins
},
Subcommand {
name: "render",
usage: "render (FILE | --sample) [--slide N | --all] [--scale S] [--edit] OUT",
usage: "render (FILE | --sample) [--slide N | --all] [--scale S] [--edit] [--transparent] OUT",
summary: "draw slides as PNG images",
details: "Draws slides of FILE, or of the sample presentation with --sample, as PNG images. FILE comes first;
OUT is the image to write. With --all, OUT is a folder that gets one slide-01.png, slide-02.png… per
slide. Prints each image's path and size.
--slide N the slide at index N; indices start at 0, and 0 is the default
--scale S pixels per point, 2 by default: a 960×540 slide becomes 1920×1080
--edit draw what the editor shows: placeholder prompts and their dashed outlines
--slide N the slide at index N; indices start at 0, and 0 is the default
--scale S pixels per point, 2 by default: a 960×540 slide becomes 1920×1080
--edit draw what the editor shows: placeholder prompts and their dashed outlines
--transparent leave the slide background out, so only what is on the slide is drawn
deckcraft-cli render deck.deckcraft --slide 2 slide-3.png",
},
+18 -1
View File
@@ -244,11 +244,13 @@ fn render(args: &[String]) -> Result<(), String> {
let mut all = false;
let mut scale = 2.0;
let mut edit = false;
let mut transparent = false;
let mut out = None;
let mut it = args.iter();
while let Some(a) = it.next() {
match a.as_str() {
"--sample" => sample = true,
"--transparent" => transparent = true,
"--slide" => slide = Some(it.next().and_then(|v| v.parse().ok()).ok_or("--slide needs a number")?),
"--all" => all = true,
"--edit" => edit = true,
@@ -281,7 +283,7 @@ fn render(args: &[String]) -> Result<(), String> {
std::fs::create_dir_all(&out).map_err(|e| format!("{out}: {e}"))?;
}
for i in list {
let (png, w, h) = deckcraft_engine::cmd::file::render_png(&st.doc, i, scale, edit);
let (png, w, h) = deckcraft_engine::cmd::file::render_png_with(&st.doc, i, scale, edit, transparent);
let path = if all { format!("{out}/slide-{:02}.png", i + 1) } else { out.clone() };
std::fs::write(&path, png).map_err(|e| format!("{path}: {e}"))?;
outln!("{path} ({w}×{h})");
@@ -680,6 +682,21 @@ mod tests {
}
}
/// `render --transparent` leaves the slide background out (#784): the corner is transparent.
#[test]
fn render_transparent_leaves_the_background_out() {
let dir = std::env::temp_dir().join(format!("deckcraft-cli-transparent-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let alpha = |path: &std::path::Path| deckcraft_engine::render::decode_image(&std::fs::read(path).unwrap()).unwrap().get_pixel(1, 1).0[3];
let opaque = dir.join("opaque.png");
render(&args(&["--sample", "--scale", "0.25", &opaque.to_string_lossy()])).unwrap();
assert_eq!(alpha(&opaque), 255);
let clear = dir.join("clear.png");
render(&args(&["--sample", "--transparent", "--scale", "0.25", &clear.to_string_lossy()])).unwrap();
assert_eq!(alpha(&clear), 0);
let _ = std::fs::remove_dir_all(&dir);
}
/// `run --export OUT.gif --export-params JSON` and `convert IN OUT.gif` write animated GIFs.
#[test]
fn exports_an_animated_gif() {
+19 -5
View File
@@ -33,7 +33,7 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!(noundo "file.saveAs", "Save As…", ["File"], Some("Cmd+Shift+S"), "{path, format?}", has_doc, save_as),
cmd!(noundo "file.saveTemplate", "Save as Template…", ["File"], None, "{path}", has_doc, save_as),
cmd!(query "file.saveBytes", "Save to Data", [], None, "{format?: deckcraft|pptx} → {data: base64}", has_doc, save_bytes_cmd),
cmd!(noundo "file.export", "Export…", ["File"], None, "{path, format?: png|jpeg|pptx|deckcraft|outline|pdf|gif (else by path), slide?: index, all?: bool, scale?: px per pt, layout?: slides|notes|handouts (pdf), perPage?: 1|2|3|4|6|9, paper?: letter|a4|legal|a3 (pdf), orientation?: portrait|landscape (pdf), dpi?, slides?: [index]|range (\"1-3,5\"), includeHidden?, textLayer?, frame?, width?: px 16-3840|small|medium|large|extraLarge (gif), secondsPerSlide?: 0.1-600 (gif; 5), loop?: bool (gif; true: forever)} → {path, bytes} or {files} (png, jpeg); gif adds frames, width, height, secondsPerSlide", has_doc, export),
cmd!(noundo "file.export", "Export…", ["File"], None, "{path, format?: png|jpeg|pptx|deckcraft|outline|pdf|gif (else by path), slide?: index, all?: bool, scale?: px per pt, layout?: slides|notes|handouts (pdf), perPage?: 1|2|3|4|6|9, paper?: letter|a4|legal|a3 (pdf), orientation?: portrait|landscape (pdf), dpi?, slides?: [index]|range (\"1-3,5\"), includeHidden?, textLayer?, frame?, transparentBackground?: bool (png: leave the slide background out), width?: px 16-3840|small|medium|large|extraLarge (gif), secondsPerSlide?: 0.1-600 (gif; 5), loop?: bool (gif; true: forever)} → {path, bytes} or {files} (png, jpeg); gif adds frames, width, height, secondsPerSlide", has_doc, export),
cmd!(
query "file.info",
"Info",
@@ -43,7 +43,7 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
info
),
cmd!(query "file.render", "Render Slide", [], None, "{slide?: index, scale?, edit?: bool} → {png: base64, width, height}", has_doc, render),
cmd!(query "file.render", "Render Slide", [], None, "{slide?: index, scale?, edit?: bool, transparentBackground?: bool} → {png: base64, width, height}", has_doc, render),
cmd!(
query "slide.colorAt",
"Color At",
@@ -431,8 +431,14 @@ pub fn image_scale(doc: &Presentation, scale: f64) -> f64 {
/// Render slide `index` (current when absent) to PNG bytes.
pub fn render_png(doc: &Presentation, index: usize, scale: f64, edit: bool) -> (Vec<u8>, u32, u32) {
render_png_with(doc, index, scale, edit, false)
}
/// [`render_png`]; `transparent` leaves the slide background out (#784).
pub fn render_png_with(doc: &Presentation, index: usize, scale: f64, edit: bool, transparent: bool) -> (Vec<u8>, u32, u32) {
let scale = image_scale(doc, scale);
let img = deckcraft_render::render_slide(doc, index, &deckcraft_render::RenderOpts { scale, edit, ..Default::default() });
let opts = deckcraft_render::RenderOpts { scale, edit, transparent_background: transparent, ..Default::default() };
let img = deckcraft_render::render_slide(doc, index, &opts);
(img.to_png_at(deckcraft_render::dpi_for_scale(scale)), img.width, img.height)
}
@@ -451,7 +457,7 @@ fn render(s: &mut Session, p: &Value) -> Result<Value> {
if i >= st.doc.slides.len() {
return Err(bad("file.render", format!("no slide {i}")));
}
let (png, w, h) = render_png(&st.doc, i, f64_or(p, "scale", 1.0), bool_or(p, "edit", false));
let (png, w, h) = render_png_with(&st.doc, i, f64_or(p, "scale", 1.0), bool_or(p, "edit", false), bool_or(p, "transparentBackground", false));
Ok(json!({"png": base64_encode(&png), "width": w, "height": h}))
}
@@ -483,6 +489,10 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
let format = str_param(p, "format").unwrap_or(format_for_path(&path)).to_string();
let st = s.doc()?;
let scale = finite_scale(f64_or(p, "scale", 2.0));
let transparent = bool_or(p, "transparentBackground", false);
if transparent && format != "png" {
return Err(bad("file.export", format!("a transparent background needs PNG; {format} can't keep transparency")));
}
match format.as_str() {
"png" | "jpeg" | "jpg" => {
let slides: Vec<usize> = match usize_param(p, "slide") {
@@ -497,7 +507,11 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
let scale = image_scale(&st.doc, scale);
let mut written = vec![];
for (k, i) in slides.iter().enumerate() {
let img = deckcraft_render::render_slide(&st.doc, *i, &deckcraft_render::RenderOpts { scale, ..Default::default() });
let img = deckcraft_render::render_slide(
&st.doc,
*i,
&deckcraft_render::RenderOpts { scale, transparent_background: transparent, ..Default::default() },
);
let bytes = picture_bytes(&img, &format, scale);
let out = if slides.len() == 1 {
path.clone()
+27
View File
@@ -2777,6 +2777,33 @@ fn a_png_export_records_the_slide_resolution() {
let _ = std::fs::remove_dir_all(&dir);
}
/// #784: `transparentBackground` leaves the slide background out of a PNG export, so only the
/// slide's content is opaque; formats without transparency refuse it and write nothing.
#[test]
fn a_png_export_can_leave_the_background_transparent() {
let mut s = session();
s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 200, 100]})).unwrap();
s.execute("design.background", &json!({"color": "#0000C8"})).unwrap();
let dir = std::env::temp_dir().join(format!("deckcraft-transparent-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let read = |path: &std::path::Path| image::load_from_memory(&std::fs::read(path).unwrap()).unwrap().to_rgba8();
let opaque = dir.join("opaque.png");
s.execute("file.export", &json!({"path": opaque.to_str().unwrap(), "scale": 1})).unwrap();
assert_eq!(read(&opaque).get_pixel(5, 5).0, [0, 0, 200, 255], "the background draws by default");
let clear = dir.join("clear.png");
s.execute("file.export", &json!({"path": clear.to_str().unwrap(), "scale": 1, "transparentBackground": true})).unwrap();
let img = read(&clear);
assert_eq!(img.get_pixel(5, 5).0[3], 0, "no background: {:?}", img.get_pixel(5, 5));
assert_eq!(img.get_pixel(200, 150).0[3], 255, "the shape stays opaque: {:?}", img.get_pixel(200, 150));
for (name, format) in [("slide.jpg", "jpeg"), ("deck.pdf", "pdf"), ("deck.gif", "gif"), ("deck.pptx", "pptx")] {
let path = dir.join(name);
let e = s.execute("file.export", &json!({"path": path.to_str().unwrap(), "format": format, "transparentBackground": true})).unwrap_err();
assert!(e.to_string().contains("needs PNG"), "{format}: {e}");
assert!(!path.exists(), "{format}: nothing is written");
}
let _ = std::fs::remove_dir_all(&dir);
}
fn png_size(bytes: &[u8]) -> Option<(u32, u32)> {
let w: [u8; 4] = bytes.get(16..20)?.try_into().ok()?;
let h: [u8; 4] = bytes.get(20..24)?.try_into().ok()?;
+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,
}
}
+11 -1
View File
@@ -175,6 +175,8 @@ pub struct Backstage {
pub file_type: &'static str,
pub handouts_per_page: u8,
pub pdf_hidden: bool,
/// Export › Change File Type › PNG: leave the slide background out (#784).
pub png_transparent: bool,
}
impl Backstage {
@@ -197,6 +199,7 @@ impl Backstage {
file_type: "pptx",
handouts_per_page: 3,
pdf_hidden: false,
png_transparent: false,
}
}
@@ -1310,8 +1313,15 @@ fn export(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage) {
}
}
ui.add_space(12.0);
let png = bs.file_type == "png";
if png {
ui.checkbox(&mut bs.png_transparent, "Transparent background")
.on_hover_text("Leave the slide background out: only what is on the slide is drawn");
ui.add_space(8.0);
}
if square_button(ui, Icon::SaveAs, "Save As", true).clicked() {
app.export_as(bs.file_type, json!({}));
let extra = if png && bs.png_transparent { json!({"transparentBackground": true}) } else { json!({}) };
app.export_as(bs.file_type, extra);
}
}
}
+26
View File
@@ -302,6 +302,32 @@ fn save_as_and_export_run_the_file_commands() {
let _ = std::fs::remove_dir_all(&dir);
}
/// Export › Change File Type › PNG offers "Transparent background" (#784): the PNG it saves
/// leaves the slide background out. Other types don't show the option.
#[test]
fn change_file_type_png_can_leave_the_background_transparent() {
let dir = temp_dir("png-clear");
let (services, _) = saving_into(&dir);
let mut h = harness(SlideApp::new(Session::with_new(), services));
h.state_mut().run("app.backstage", json!({"page": "export"})).unwrap();
h.run_steps(3);
click_on_page(&h, "Change File Type");
h.run_steps(3);
assert!(h.query_by_label("Transparent background").is_none(), "only PNG has the option");
click_on_page(&h, "PNG Picture (*.png)");
h.run_steps(2);
h.get_by_label("Transparent background").click();
h.run_steps(2);
assert!(h.state().backstage.as_ref().is_some_and(|b| b.png_transparent));
click_on_page(&h, "Save As");
h.run_steps(3);
let title = h.state().session.active().unwrap().title();
let png = std::fs::read(dir.join(format!("{title}.png"))).unwrap();
let img = deckcraft_render::decode_image(&png).unwrap();
assert_eq!(img.get_pixel(2, 2).0[3], 0, "the background is left out");
let _ = std::fs::remove_dir_all(&dir);
}
/// Close closes a saved presentation at once and asks about unsaved changes first.
#[test]
fn close_closes_and_asks_about_unsaved_changes() {
+54
View File
@@ -1593,6 +1593,18 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
d.fields.insert("pdfHidden".into(), hidden.to_string());
}
}
let png = matches!(fmt.as_str(), "png" | "pngAll");
if png {
ui.add_space(4.0);
let mut clear = d.get("pngTransparent", "false") == "true";
if ui
.checkbox(&mut clear, "Transparent background")
.on_hover_text("Leave the slide background out: only what is on the slide is drawn (#784)")
.changed()
{
d.fields.insert("pngTransparent".into(), clear.to_string());
}
}
let gif_params = if fmt == "gif" { gif_options(app, ui, d) } else { Ok(Value::Null) };
if let Err(e) = &gif_params {
ui.label(egui::RichText::new(e).color(Tokens::get(ui.ctx()).text_dim));
@@ -1618,6 +1630,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let extra = match fmt.as_str() {
"pdf" => pdf_params,
"gif" => gif_params,
_ if png && d.get("pngTransparent", "false") == "true" => json!({"transparentBackground": true}),
_ => json!({}),
};
app.export_as(&fmt, extra);
@@ -3432,6 +3445,47 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// The Export… dialog offers "Transparent background" for PNG only, and the picture it
/// writes then leaves the slide background out (#784).
#[test]
fn export_dialog_writes_a_png_with_a_transparent_background() {
use egui::vec2;
use egui_kittest::kittest::Queryable as _;
use serde_json::json;
use crate::SlideApp;
let dir = std::env::temp_dir().join(format!("deckcraft-ui-png-clear-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("slide.png");
let picked = path.to_string_lossy().to_string();
let services = crate::Services { pick_save: Some(Box::new(move |_: &str| Some(picked.clone()))), ..Default::default() };
let mut harness = egui_kittest::Harness::builder().with_size(vec2(1200.0, 900.0)).build_ui_state(
|ui, app: &mut SlideApp| {
app.logic(ui.ctx());
app.ui(ui);
},
SlideApp::new(deckcraft_engine::Session::with_new(), services),
);
harness.run_steps(2);
harness.state_mut().run("app.exportDialog", json!({})).unwrap();
harness.run_steps(2);
assert!(harness.query_by_label("Transparent background").is_none(), "PDF has no transparent background");
harness.get_by_label("JPEG image (current slide)").click_accesskit();
harness.run_steps(2);
assert!(harness.query_by_label("Transparent background").is_none(), "nor has JPEG");
harness.get_by_label("PNG images (current slide)").click_accesskit();
harness.run_steps(2);
harness.get_by_label("Transparent background").click_accesskit();
harness.run_steps(2);
assert_eq!(harness.state().dialog.as_ref().and_then(|d| d.fields.get("pngTransparent")).map(String::as_str), Some("true"));
harness.key_press(egui::Key::Enter);
harness.run_steps(2);
assert!(harness.state().dialog.is_none(), "the dialog closes after exporting");
let img = deckcraft_render::decode_image(&std::fs::read(&path).unwrap()).unwrap();
assert_eq!(img.get_pixel(2, 2).0[3], 0, "the background is left out");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn no_link_opens_as_a_new_web_address() {
let seed = seed_from(CurrentLink::None, &[]);
+40 -6
View File
@@ -769,7 +769,8 @@ impl SlideApp {
// Every slide for "all slides", one download each, named like the desktop export (#64).
let name = self.session.active().map(|d| d.title()).unwrap_or_else(|| "Presentation".into());
let mut any = false;
for (file, bytes) in web_image_files(&doc, slide, all, &name, ext) {
let transparent = extra.get("transparentBackground").and_then(Value::as_bool).unwrap_or(false);
for (file, bytes) in web_image_files(&doc, slide, all, &name, ext, transparent) {
any = self.download(&file, Ok(bytes)) || any;
}
return any;
@@ -1987,8 +1988,16 @@ pub fn now_ms() -> f64 {
}
/// Images for the browser download: the current slide, or every slide (named `<name>1.png`,
/// `<name>2.png`... like the native export) when `all`.
fn web_image_files(doc: &deckcraft_model::Presentation, current: usize, all: bool, name: &str, ext: &str) -> Vec<(String, Vec<u8>)> {
/// `<name>2.png`... like the native export) when `all`. `transparent` leaves the slide
/// background out (PNG only, #784).
fn web_image_files(
doc: &deckcraft_model::Presentation,
current: usize,
all: bool,
name: &str,
ext: &str,
transparent: bool,
) -> Vec<(String, Vec<u8>)> {
let slides: Vec<usize> = if all { (0..doc.slides.len()).collect() } else { vec![current] };
let single = slides.len() == 1;
let scale = 2.0;
@@ -1997,7 +2006,8 @@ fn web_image_files(doc: &deckcraft_model::Presentation, current: usize, all: boo
.into_iter()
.enumerate()
.map(|(k, i)| {
let img = deckcraft_render::render_slide(doc, i, &deckcraft_render::RenderOpts { scale, ..Default::default() });
let opts = deckcraft_render::RenderOpts { scale, transparent_background: transparent && ext == "png", ..Default::default() };
let img = deckcraft_render::render_slide(doc, i, &opts);
let bytes = if ext == "png" { img.to_png_at(dpi) } else { img.to_jpeg_at(92, Some(dpi)) };
let file = if single { format!("{name}.{ext}") } else { format!("{name}{}.{ext}", k + 1) };
(file, bytes)
@@ -2098,14 +2108,38 @@ mod tests {
doc.slides.push(first);
}
assert_eq!(doc.slides.len(), 3);
let all = web_image_files(&doc, 1, true, "Deck", "png");
let all = web_image_files(&doc, 1, true, "Deck", "png", false);
let names: Vec<&str> = all.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(names, ["Deck1.png", "Deck2.png", "Deck3.png"]);
let one = web_image_files(&doc, 1, false, "Deck", "png");
let one = web_image_files(&doc, 1, false, "Deck", "png", false);
assert_eq!(one.len(), 1);
assert_eq!(one[0].0, "Deck.png");
}
/// The web PNG download honours "Transparent background" (#784): the slide background is
/// left out, so the corner is transparent; a JPEG download stays opaque.
#[test]
fn web_png_export_can_leave_the_background_transparent() {
use std::cell::RefCell;
use std::rc::Rc;
let got: Rc<RefCell<Vec<(String, Vec<u8>)>>> = Rc::default();
let log = got.clone();
let services = Services {
download: Some(Box::new(move |name: &str, b: &[u8]| log.borrow_mut().push((name.to_string(), b.to_vec())))),
..Default::default()
};
let mut app = SlideApp::new(Session::with_new(), services);
let corner_alpha = |bytes: &[u8]| deckcraft_render::decode_image(bytes).unwrap().get_pixel(2, 2).0[3];
assert!(app.export_as("png", json!({})));
assert!(app.export_as("png", json!({"transparentBackground": true})));
assert!(app.export_as("jpeg", json!({"transparentBackground": true})));
let got = got.borrow();
assert_eq!(got.len(), 3);
assert_eq!(corner_alpha(&got[0].1), 255, "opaque by default");
assert_eq!(corner_alpha(&got[1].1), 0, "transparent background");
assert_eq!(corner_alpha(&got[2].1), 255, "JPEG has no transparency");
}
fn title_text(app: &SlideApp) -> String {
let d = app.session.active().unwrap();
let sh = &d.doc.slides[0].shapes[0];