collective

Browse / github.com/storytold/vectorcraft / patch

Raster export options show estimated file size (#4246)

Feature request: Display estimated file size in the export dialog before exporting ## Is your feature request related to a problem? Please describe. Yes. Currently, when exporting any artwork from Vector Craft, the application does not display the estimated file size in the export settings dialog. This is a significant issue because users frequently need to target specific file sizes (e.g., for web optimization, email attachments, or platform limits) and can only verify the final size after the file has already been saved to their drive. ## Describe the solution you'd like I would like to see a dynamic "Estimated File Size" indicator added directly inside the export options

Status
open
Score
0.00
Adopters
0 other operators
Author
nrq6993-a11y (imported from pull request #4291, open)
Base
e2a8280134a7c3c9b4e117ee668b9b3e7eb3e315
Size
+52 −1 in 2 files
Review
pending
Shared
2026-10-28
Id
39cadc95f3385a1d6f5f5fa5

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/dialogs/png_options.rs crates/ui-egui/src/dialogs/tests_export.rs

diff --git a/crates/ui-egui/src/dialogs/png_options.rs b/crates/ui-egui/src/dialogs/png_options.rs--- a/crates/ui-egui/src/dialogs/png_options.rs+++ b/crates/ui-egui/src/dialogs/png_options.rs@@ -128,7 +128,40 @@ fn heading(d: &Dialog) -> String {     crate::i18n::fmt(tl!("{format} Options"), &[("format", format_of(d).map_or(tl!("Export"), |f| f.label))]) } -fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {+/// A byte count as the estimated-size row shows it (Save for Web uses the same bands).+fn size_label(bytes: usize) -> String {+    if bytes < 1024 {+        format!("{bytes} bytes")+    } else if bytes < 1 << 20 {+        format!("{:.1}K", bytes as f64 / 1024.0)+    } else {+        format!("{:.2}M", bytes as f64 / f64::from(1 << 20))+    }+}++/// Encode with the current options (no file written) and remember the size on the dialog.+fn refresh_estimated_size(app: &mut VectorcraftApp, d: &mut Dialog) {+    let raster = format_of(d).is_some_and(|f| matches!(f.id, "png" | "jpg" | "webp" | "gif" | "png8" | "tiff" | "bmp" | "tga" | "psd"));+    if !raster {+        d.fields.remove("__estimateKey");+        d.fields.remove("__estimatedBytes");+        return;+    }+    let mut params = form::params(d);+    if let Some(o) = params.as_object_mut() {+        o.remove("path");+    }+    let key = params.to_string();+    if d.str("__estimateKey") == key && d.fields.contains_key("__estimatedBytes") {+        return;+    }+    let bytes = app.run("document.export", params).ok().and_then(|r| r.get("bytes").and_then(Value::as_u64));+    d.fields.insert("__estimateKey".into(), json!(key));+    d.fields.insert("__estimatedBytes".into(), json!(bytes));+}++fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {+    refresh_estimated_size(app, d);     let t = Tokens::get(ui.ctx());     let id = format_of(d).map_or("png", |f| f.id);     let label = |ui: &mut egui::Ui, text: &str| ui.label(egui::RichText::new(tl!(text)).color(t.text_dim));@@ -161,6 +194,11 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {             ui.label(egui::RichText::new(format!("{pw} × {ph} px")).color(t.text));             ui.end_row();         }+        if let Some(bytes) = d.fields.get("__estimatedBytes").and_then(Value::as_u64) {+            label(ui, tl!("Estimated File Size:"));+            ui.label(egui::RichText::new(size_label(bytes as usize)).color(t.text));+            ui.end_row();+        }     });     false }diff --git a/crates/ui-egui/src/dialogs/tests_export.rs b/crates/ui-egui/src/dialogs/tests_export.rs--- a/crates/ui-egui/src/dialogs/tests_export.rs+++ b/crates/ui-egui/src/dialogs/tests_export.rs@@ -67,6 +67,7 @@ fn png_options_draw_and_export_with_the_chosen_options() {     assert_eq!((d.f64("ppi", 0.0), d.str("background"), d.str("antiAlias"), d.bool("interlaced")), (72.0, "transparent".into(), "art".into(), false));     assert_eq!(d.fields["__size"], json!([60.0, 40.0]), "the chosen artboard");     frame(&mut app);+    assert!(app.ui.dialog.as_ref().unwrap().fields.get("__estimatedBytes").and_then(|v| v.as_u64()).is_some());     // Other resolution and an Other background colour draw their own field and colour button.     set(&mut app, "__otherPpi", json!(true));     set(&mut app, "ppi", json!(200));@@ -93,6 +94,18 @@ fn png_options_draw_and_export_with_the_chosen_options() {     assert_eq!(&written.borrow()[1].1[..2], [0xFF, 0xD8]); } +#[test]+fn png_options_estimated_file_size_follows_resolution() {+    let (mut app, _) = app(1);+    png_options::open(&mut app, fileio::format("png").unwrap(), json!({"format": "png", "path": "/out/a.png"}));+    frame(&mut app);+    let small = app.ui.dialog.as_ref().unwrap().fields["__estimatedBytes"].as_u64().unwrap();+    set(&mut app, "ppi", json!(300.0));+    frame(&mut app);+    let large = app.ui.dialog.as_ref().unwrap().fields["__estimatedBytes"].as_u64().unwrap();+    assert!(large > small, "{large} vs {small}");+}+ #[test] fn export_as_png_picks_the_file_then_shows_png_options() {     let (mut app, written) = app(1);