From 0dbae06c4512dbba703a5eb75d08371a4796b627 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Mon, 27 Jul 2026 16:05:49 +0200 Subject: [PATCH] :bug: Fix client-side wasm export encoding jpeg and webp as png --- frontend/src/app/main/data/exports/wasm.cljs | 5 ++++- .../main/data/workspace/thumbnails_wasm.cljs | 4 +++- frontend/src/app/render_wasm/api.cljs | 8 ++++++-- render-wasm/src/main.rs | 8 +++++++- render-wasm/src/render.rs | 17 +++++++++++++---- render-wasm/src/state.rs | 3 ++- 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/main/data/exports/wasm.cljs b/frontend/src/app/main/data/exports/wasm.cljs index b91f461795..4cbd5285f8 100644 --- a/frontend/src/app/main/data/exports/wasm.cljs +++ b/frontend/src/app/main/data/exports/wasm.cljs @@ -13,7 +13,10 @@ (defn export-image-uri [{:keys [type scale object-id]}] - (let [bytes (wasm.api/render-shape-pixels object-id scale) + ;; The export type doubles as the encoder format, so the bytes always match + ;; the mtype we wrap them in. `:svg` never reaches here — it needs vector + ;; markup and `exports.assets/wasm-export-types` keeps it off this path. + (let [bytes (wasm.api/render-shape-pixels object-id scale type) mtype (format->mtype type) blob (wapi/create-blob bytes mtype)] (wapi/create-uri blob))) diff --git a/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs b/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs index 2755c530fd..edef890e1e 100644 --- a/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs @@ -68,7 +68,9 @@ {sel-w :width sel-h :height} (:selrect frame) max-size (mth/max (or ext-w sel-w) (or ext-h sel-h)) scale (mth/max 1 (/ target-size max-size)) - png-bytes (wasm.api/render-shape-pixels frame-id scale)] + ;; Thumbnails are always PNG: they need the alpha + ;; channel and are consumed as a `image/png` data uri. + png-bytes (wasm.api/render-shape-pixels frame-id scale :png)] (if (or (nil? png-bytes) (zero? (.-length png-bytes))) (do (l/error :hint "render-shape-pixels returned empty" :frame-id (str frame-id)) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 0e46a2439f..575a61bb0a 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -2629,7 +2629,10 @@ (reset! transition-image* snapshot))))) (defn render-shape-pixels - [shape-id scale] + "Renders a shape subtree to encoded image bytes. `format` is :png, :jpeg or + :webp; jpeg is flattened onto white on the Rust side, since it has no alpha + channel." + [shape-id scale format] (let [buffer (uuid/get-u32 shape-id) offset @@ -2638,7 +2641,8 @@ (aget buffer 1) (aget buffer 2) (aget buffer 3) - scale) + scale + (sr/translate-raster-format format)) heap (mem/get-heap-u8) heapu32 (mem/get-heap-u32) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index c95ba526d9..2d007d18e4 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -976,6 +976,9 @@ pub extern "C" fn get_shape_extrect(a: u32, b: u32, c: u32, d: u32) -> Result<*m }) } +/// Raster image via the GPU surface. Returns `[len][width][height][bytes]` +/// (LE). `format` selects the encoder: 0 = PNG, 1 = JPEG, 2 = WEBP (see +/// `RasterFormat`). #[no_mangle] #[wasm_error] pub extern "C" fn render_shape_pixels( @@ -984,6 +987,7 @@ pub extern "C" fn render_shape_pixels( c: u32, d: u32, scale: f32, + format: u32, ) -> Result<*mut u8> { let id = uuid_from_u32_quartet(a, b, c, d); @@ -991,9 +995,11 @@ pub extern "C" fn render_shape_pixels( return Err(Error::CriticalError("Scale is not finite".to_string())); } + let format = RasterFormat::from_u32(format)?; + with_state!(state, { let (data, width, height) = - state.render_shape_pixels(&id, scale, performance::get_time())?; + state.render_shape_pixels(&id, scale, performance::get_time(), format)?; let len = data.len() as u32; let mut buf = Vec::with_capacity(4 + data.len()); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index b522d5c4f0..7ff4423f7a 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -2429,6 +2429,7 @@ impl RenderState { tree: ShapesPoolRef, scale: f32, timestamp: i32, + format: raster::RasterFormat, ) -> Result<(Vec, i32, i32)> { let target_surface = SurfaceId::Export; @@ -2459,7 +2460,7 @@ impl RenderState { self.surfaces .canvas(target_surface) - .clear(skia::Color::TRANSPARENT); + .clear(format.clear_color()); if tree.len() != 0 { let Some(shape) = tree.get(id) else { @@ -2476,6 +2477,14 @@ impl RenderState { self.render_area_with_margins = extrect; self.surfaces.update_render_context(extrect, scale); + // `resize_export_surface` swaps in a brand-new (zeroed, i.e. + // transparent) surface whenever the dimensions change, discarding + // the clear above — so an opaque backdrop has to be laid down again + // here, after the resize. + if let Some(background) = format.opaque_background() { + self.surfaces.canvas(target_surface).clear(background); + } + self.pending_nodes.push(NodeRenderState { id: *id, visited_children: false, @@ -2496,10 +2505,10 @@ impl RenderState { let data = image .encode( Some(&mut get_gpu_state().context), - skia::EncodedImageFormat::PNG, - 100, + format.encoded(), + format.quality(), ) - .expect("PNG encode failed"); + .unwrap_or_else(|| panic!("{format:?} encode failed")); let skia::ISize { width, height } = image.dimensions(); // Restore the workspace render state. diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index fb277f2fdc..849d35131b 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -96,8 +96,9 @@ impl State { id: &Uuid, scale: f32, timestamp: i32, + format: RasterFormat, ) -> Result<(Vec, i32, i32)> { - get_render_state().render_shape_pixels(id, &self.shapes, scale, timestamp) + get_render_state().render_shape_pixels(id, &self.shapes, scale, timestamp, format) } pub fn render_shape_pdf(&mut self, id: &Uuid, scale: f32) -> Result> {