🐛 Export bitmaps over the GPU surface limit through the exporter (#12455)

This commit is contained in:
Elena Torró 2026-10-20 15:37:07 +02:00 committed by GitHub
parent ef68304cb8
commit a1606c42ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 92 additions and 8 deletions

View File

@ -18,6 +18,7 @@
[app.main.features :as features]
[app.main.repo :as rp]
[app.main.store :as st]
[app.render-wasm.api :as wasm.api]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.websocket :as ws]
@ -300,11 +301,18 @@
[state]
(features/active-feature? state "render-wasm/v1"))
(defn- use-wasm-export?
"Whether to take the client-side WASM export path for `export`."
[state export]
(and (wasm-export-enabled? state)
(contains? wasm-export-types (:type export))))
(defn- export-target
[state {:keys [type object-id scale]}]
(cond
(not (and (wasm-export-enabled? state)
(contains? wasm-export-types type)))
:exporter
(contains? #{:pdf :svg} type)
:client
:else
(or (wasm.api/export-target object-id (or scale 1) type) :exporter)))
(defn- request-simple-export-wasm
[export]
@ -318,18 +326,25 @@
(defn request-simple-export
[{:keys [export]}]
(let [export (normalize-export export)]
(let [export (normalize-export export)
target (volatile! nil)]
(ptk/reify ::request-simple-export
ptk/UpdateEvent
(update [_ state]
(vreset! target (export-target state export))
(cond-> state
(not (use-wasm-export? state export))
(= :exporter @target)
(update :export assoc :in-progress true :id uuid/zero)))
ptk/WatchEvent
(watch [_ state _]
(if (use-wasm-export? state export)
(case @target
:client
(rx/of (request-simple-export-wasm export))
:too-large
(rx/of (ntf/error (tr "errors.export-too-large")))
(let [profile-id (:profile-id state)
params (normalize-export-shapes-params {:exports [export]
:profile-id profile-id

View File

@ -3009,6 +3009,21 @@
(mem/free)
{:x x :y y :width w :height h})))))
(defn export-target
[shape-id scale format]
(when (initialized?)
(let [buffer (uuid/get-u32 shape-id)]
(case (h/call wasm/internal-module "_export_target"
(aget buffer 0)
(aget buffer 1)
(aget buffer 2)
(aget buffer 3)
scale
(sr/translate-raster-format format))
0 :client
2 :too-large
:exporter))))
(defn render-shape-pdf
[shape-id]
(when (initialized?)

View File

@ -1586,6 +1586,10 @@ msgstr "The email «%s» has been reported as spam or permanently bounce."
msgid "errors.export-failed"
msgstr "The export failed. Try again in a moment."
#: src/app/main/data/exports/assets.cljs:346
msgid "errors.export-too-large"
msgstr "Too large to export, try a smaller scale"
#: src/app/main/errors.cljs:974
msgid "errors.feature-mismatch"
msgstr ""

View File

@ -1596,6 +1596,10 @@ msgstr "El email «%s» tiene reportes de spam o de rebote permanente."
msgid "errors.export-failed"
msgstr "La exportación ha fallado. Inténtalo de nuevo en un momento."
#: src/app/main/data/exports/assets.cljs:346
msgid "errors.export-too-large"
msgstr "El archivo es demasiado grande, prueba con otra escala"
#: src/app/main/errors.cljs:974
msgid "errors.feature-mismatch"
msgstr ""

View File

@ -806,6 +806,31 @@ pub extern "C" fn render_shape_pixels(
})
}
#[no_mangle]
pub extern "C" fn export_target(a: u32, b: u32, c: u32, d: u32, scale: f32, format: u32) -> u32 {
let id = uuid_from_u32_quartet(a, b, c, d);
let Ok(format) = RasterFormat::from_u32(format) else {
return 1;
};
with_state!(state, {
let Some(shape) = state.shapes.get(&id) else {
return 1;
};
let extrect = get_render_state().get_cached_extrect(shape, &state.shapes, scale);
let width = (scale * extrect.width()).ceil() as i32;
let height = (scale * extrect.height()).ceil() as i32;
let max_size = get_gpu_state().max_surface_size();
if width <= max_size && height <= max_size {
0
} else if format.fits_raster(width, height) {
1
} else {
2
}
})
}
#[no_mangle]
pub extern "C" fn render_stats() {
get_render_state().print_stats();

View File

@ -8,6 +8,16 @@ use macros::ToJs;
use super::vector;
use super::RenderResources;
/// Half the default 2 GiB emscripten heap (a 16384² RGBA surface), leaving
/// room for the encoded copies and the scene. Estimated, not measured.
const MAX_RASTER_BYTES: i64 = 1 << 30;
/// PNG stores each side in 31 bits; the memory budget limits it first.
const MAX_PNG_SIDE: i32 = i32::MAX;
/// libjpeg-turbo's `JPEG_MAX_DIMENSION`, below the 16-bit JPEG header limit.
const MAX_JPEG_SIDE: i32 = 65500;
/// libwebp's `WEBP_MAX_DIMENSION`: WebP stores each side in 14 bits.
const MAX_WEBP_SIDE: i32 = 16383;
/// Encoded output format for [`render_to_raster`] and `render_shape_pixels`.
/// `ToJs` publishes the discriminants to `api/shared.js`, so the CLJS side
/// reads them from here instead of hardcoding the codes.
@ -65,6 +75,17 @@ impl RasterFormat {
pub fn clear_color(self) -> skia::Color {
self.opaque_background().unwrap_or(skia::Color::TRANSPARENT)
}
pub fn fits_raster(self, width: i32, height: i32) -> bool {
let max_side = match self {
Self::Png => MAX_PNG_SIDE,
Self::Jpeg => MAX_JPEG_SIDE,
Self::Webp => MAX_WEBP_SIDE,
};
width <= max_side
&& height <= max_side
&& width as i64 * height as i64 * 4 <= MAX_RASTER_BYTES
}
}
/// Renders a shape tree to encoded image bytes on a CPU raster surface (no