🐛 Fix client-side wasm export encoding jpeg and webp as png

This commit is contained in:
Elena Torro 2026-07-27 16:05:49 +02:00
parent bcc8399bcc
commit 0dbae06c45
6 changed files with 35 additions and 10 deletions

View File

@ -13,7 +13,10 @@
(defn export-image-uri (defn export-image-uri
[{:keys [type scale object-id]}] [{: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) mtype (format->mtype type)
blob (wapi/create-blob bytes mtype)] blob (wapi/create-blob bytes mtype)]
(wapi/create-uri blob))) (wapi/create-uri blob)))

View File

@ -68,7 +68,9 @@
{sel-w :width sel-h :height} (:selrect frame) {sel-w :width sel-h :height} (:selrect frame)
max-size (mth/max (or ext-w sel-w) (or ext-h sel-h)) max-size (mth/max (or ext-w sel-w) (or ext-h sel-h))
scale (mth/max 1 (/ target-size max-size)) 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))) (if (or (nil? png-bytes) (zero? (.-length png-bytes)))
(do (do
(l/error :hint "render-shape-pixels returned empty" :frame-id (str frame-id)) (l/error :hint "render-shape-pixels returned empty" :frame-id (str frame-id))

View File

@ -2629,7 +2629,10 @@
(reset! transition-image* snapshot))))) (reset! transition-image* snapshot)))))
(defn render-shape-pixels (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) (let [buffer (uuid/get-u32 shape-id)
offset offset
@ -2638,7 +2641,8 @@
(aget buffer 1) (aget buffer 1)
(aget buffer 2) (aget buffer 2)
(aget buffer 3) (aget buffer 3)
scale) scale
(sr/translate-raster-format format))
heap (mem/get-heap-u8) heap (mem/get-heap-u8)
heapu32 (mem/get-heap-u32) heapu32 (mem/get-heap-u32)

View File

@ -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] #[no_mangle]
#[wasm_error] #[wasm_error]
pub extern "C" fn render_shape_pixels( pub extern "C" fn render_shape_pixels(
@ -984,6 +987,7 @@ pub extern "C" fn render_shape_pixels(
c: u32, c: u32,
d: u32, d: u32,
scale: f32, scale: f32,
format: u32,
) -> Result<*mut u8> { ) -> Result<*mut u8> {
let id = uuid_from_u32_quartet(a, b, c, d); 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())); return Err(Error::CriticalError("Scale is not finite".to_string()));
} }
let format = RasterFormat::from_u32(format)?;
with_state!(state, { with_state!(state, {
let (data, width, height) = 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 len = data.len() as u32;
let mut buf = Vec::with_capacity(4 + data.len()); let mut buf = Vec::with_capacity(4 + data.len());

View File

@ -2429,6 +2429,7 @@ impl RenderState {
tree: ShapesPoolRef, tree: ShapesPoolRef,
scale: f32, scale: f32,
timestamp: i32, timestamp: i32,
format: raster::RasterFormat,
) -> Result<(Vec<u8>, i32, i32)> { ) -> Result<(Vec<u8>, i32, i32)> {
let target_surface = SurfaceId::Export; let target_surface = SurfaceId::Export;
@ -2459,7 +2460,7 @@ impl RenderState {
self.surfaces self.surfaces
.canvas(target_surface) .canvas(target_surface)
.clear(skia::Color::TRANSPARENT); .clear(format.clear_color());
if tree.len() != 0 { if tree.len() != 0 {
let Some(shape) = tree.get(id) else { let Some(shape) = tree.get(id) else {
@ -2476,6 +2477,14 @@ impl RenderState {
self.render_area_with_margins = extrect; self.render_area_with_margins = extrect;
self.surfaces.update_render_context(extrect, scale); 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 { self.pending_nodes.push(NodeRenderState {
id: *id, id: *id,
visited_children: false, visited_children: false,
@ -2496,10 +2505,10 @@ impl RenderState {
let data = image let data = image
.encode( .encode(
Some(&mut get_gpu_state().context), Some(&mut get_gpu_state().context),
skia::EncodedImageFormat::PNG, format.encoded(),
100, format.quality(),
) )
.expect("PNG encode failed"); .unwrap_or_else(|| panic!("{format:?} encode failed"));
let skia::ISize { width, height } = image.dimensions(); let skia::ISize { width, height } = image.dimensions();
// Restore the workspace render state. // Restore the workspace render state.

View File

@ -96,8 +96,9 @@ impl State {
id: &Uuid, id: &Uuid,
scale: f32, scale: f32,
timestamp: i32, timestamp: i32,
format: RasterFormat,
) -> Result<(Vec<u8>, i32, i32)> { ) -> Result<(Vec<u8>, 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<Vec<u8>> { pub fn render_shape_pdf(&mut self, id: &Uuid, scale: f32) -> Result<Vec<u8>> {