Skip to main content

oxiui_render_soft/
canvas_upload.rs

1//! Canvas 2D pixel upload path — write a [`Framebuffer`] to an HTML canvas element.
2//!
3//! This module provides a `putImageData`-based upload path as an alternative
4//! to WebGPU for environments that support Canvas 2D but not WebGPU (older
5//! browsers, embedded environments, certain CI configurations).
6//!
7//! # Platform notes
8//!
9//! - **wasm32**: the `upload_framebuffer` and `upload_rgba` functions are fully
10//!   implemented using `web_sys::CanvasRenderingContext2d` and `ImageData`.
11//! - **native**: both functions are no-ops that always return `Ok(())`.
12//!   This enables `--all-features` native builds to compile successfully.
13//!
14//! # Feature gate
15//!
16//! Enable the `canvas-2d` Cargo feature to activate this module.  Without that
17//! feature, the module is still compiled but contains only the native stubs.
18//!
19//! # Example (wasm32, `canvas-2d` feature)
20//!
21//! ```rust,ignore
22//! use oxiui_render_soft::{Framebuffer, canvas_upload::upload_framebuffer};
23//! use oxiui_core::Color;
24//!
25//! let fb = Framebuffer::with_fill(320, 240, Color(30, 30, 30, 255));
26//! upload_framebuffer(&fb, "my-canvas").expect("canvas upload failed");
27//! ```
28
29use crate::framebuffer::Framebuffer;
30
31// ---------------------------------------------------------------------------
32// wasm32 implementation
33// ---------------------------------------------------------------------------
34
35/// Upload the pixel contents of `fb` to the HTML canvas element with the
36/// given `canvas_id` using `CanvasRenderingContext2d.putImageData`.
37///
38/// The canvas is resized to `fb.width() × fb.height()` before the upload.
39///
40/// # Errors
41///
42/// Returns a descriptive `String` error if:
43/// * The `canvas_id` element is not found in the DOM.
44/// * The element is not an `<canvas>` element.
45/// * The 2D rendering context cannot be obtained.
46/// * `ImageData` construction fails (browser restriction or out-of-memory).
47#[cfg(all(feature = "canvas-2d", target_arch = "wasm32"))]
48pub fn upload_framebuffer(fb: &Framebuffer, canvas_id: &str) -> Result<(), String> {
49    let rgba_buf = framebuffer_to_rgba8(fb);
50    upload_rgba(&rgba_buf, fb.width(), fb.height(), canvas_id)
51}
52
53/// Upload a raw RGBA8 byte slice to the given canvas element.
54///
55/// `data` must be exactly `width * height * 4` bytes in RGBA row-major order.
56///
57/// # Errors
58///
59/// Returns a descriptive error string on failure (see [`upload_framebuffer`]).
60#[cfg(all(feature = "canvas-2d", target_arch = "wasm32"))]
61pub fn upload_rgba(data: &[u8], width: u32, height: u32, canvas_id: &str) -> Result<(), String> {
62    use wasm_bindgen::JsCast;
63
64    let window =
65        web_sys::window().ok_or_else(|| "canvas_upload: no global window object".to_owned())?;
66    let document = window
67        .document()
68        .ok_or_else(|| "canvas_upload: window.document is None".to_owned())?;
69
70    // Locate the canvas element.
71    let element = document
72        .get_element_by_id(canvas_id)
73        .ok_or_else(|| format!("canvas_upload: element '{canvas_id}' not found"))?;
74    let canvas: web_sys::HtmlCanvasElement = element
75        .dyn_into::<web_sys::HtmlCanvasElement>()
76        .map_err(|_| format!("canvas_upload: '{canvas_id}' is not an <canvas>"))?;
77
78    // Resize the canvas to match the framebuffer.
79    canvas.set_width(width);
80    canvas.set_height(height);
81
82    // Obtain 2D context.
83    let ctx = canvas
84        .get_context("2d")
85        .map_err(|e| format!("canvas_upload: get_context('2d') failed: {e:?}"))?
86        .ok_or_else(|| format!("canvas_upload: '2d' context not available on '{canvas_id}'"))?
87        .dyn_into::<web_sys::CanvasRenderingContext2d>()
88        .map_err(|_| "canvas_upload: context is not CanvasRenderingContext2d".to_owned())?;
89
90    // Build ImageData from the RGBA8 byte slice.
91    // `ImageData::new_with_u8_clamped_array_and_sh` creates an ImageData from a
92    // `Uint8ClampedArray`.  The slice is copied into Wasm linear memory via
93    // `js_sys::Uint8ClampedArray::view` (zero-copy borrow).
94    //
95    // Safety: `view` is sound here because we do not invoke any JS that could
96    // cause the Wasm heap to grow between `view()` and `new_with_u8_clamped_array_and_sh()`.
97    let clamped_arr = js_sys::Uint8ClampedArray::from(data);
98    let image_data =
99        web_sys::ImageData::new_with_u8_clamped_array_and_sh(&clamped_arr, width, height)
100            .map_err(|e| format!("canvas_upload: ImageData construction failed: {e:?}"))?;
101
102    // Upload.
103    ctx.put_image_data(&image_data, 0.0, 0.0)
104        .map_err(|e| format!("canvas_upload: putImageData failed: {e:?}"))?;
105
106    Ok(())
107}
108
109// ---------------------------------------------------------------------------
110// Native stubs
111// ---------------------------------------------------------------------------
112
113/// Native-platform stub — always returns `Ok(())`.
114///
115/// On `wasm32` + `canvas-2d` the real implementation is active.
116#[cfg(not(all(feature = "canvas-2d", target_arch = "wasm32")))]
117pub fn upload_framebuffer(fb: &Framebuffer, canvas_id: &str) -> Result<(), String> {
118    // No canvas on native targets; discard the arguments explicitly.
119    let _ = (fb, canvas_id);
120    Ok(())
121}
122
123/// Native-platform stub — always returns `Ok(())`.
124///
125/// On `wasm32` + `canvas-2d` the real implementation is active.
126#[cfg(not(all(feature = "canvas-2d", target_arch = "wasm32")))]
127pub fn upload_rgba(data: &[u8], width: u32, height: u32, canvas_id: &str) -> Result<(), String> {
128    // No canvas on native targets; discard the arguments explicitly.
129    let _ = (data, width, height, canvas_id);
130    Ok(())
131}
132
133// ---------------------------------------------------------------------------
134// Shared helper
135// ---------------------------------------------------------------------------
136
137/// Convert a [`Framebuffer`] (`0xAARRGGBB`) to a flat RGBA8 byte vector
138/// (`[R, G, B, A, ...]`) as expected by `ImageData`.
139pub fn framebuffer_to_rgba8(fb: &Framebuffer) -> Vec<u8> {
140    use crate::framebuffer::unpack;
141    let mut out = Vec::with_capacity(fb.width() as usize * fb.height() as usize * 4);
142    for &px in fb.pixels() {
143        let (r, g, b, a) = unpack(px);
144        out.extend_from_slice(&[r, g, b, a]);
145    }
146    out
147}
148
149// ---------------------------------------------------------------------------
150// Tests
151// ---------------------------------------------------------------------------
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use oxiui_core::Color;
157
158    #[test]
159    fn framebuffer_to_rgba8_correct_layout() {
160        let fb = Framebuffer::with_fill(2, 1, Color(10, 20, 30, 200));
161        let raw = framebuffer_to_rgba8(&fb);
162        assert_eq!(raw.len(), 8);
163        // First pixel: R=10, G=20, B=30, A=200
164        assert_eq!(&raw[..4], &[10, 20, 30, 200]);
165        // Second pixel: same
166        assert_eq!(&raw[4..], &[10, 20, 30, 200]);
167    }
168
169    #[test]
170    fn framebuffer_to_rgba8_transparent() {
171        let fb = Framebuffer::new(1, 1); // transparent black
172        let raw = framebuffer_to_rgba8(&fb);
173        assert_eq!(raw, vec![0, 0, 0, 0]);
174    }
175
176    #[test]
177    fn upload_framebuffer_native_noop() {
178        // On native, both upload functions are no-ops and return Ok.
179        let fb = Framebuffer::with_fill(4, 4, Color(255, 0, 0, 255));
180        let result = upload_framebuffer(&fb, "test-canvas");
181        assert!(result.is_ok(), "native stub must return Ok: {result:?}");
182    }
183
184    #[test]
185    fn upload_rgba_native_noop() {
186        // 4×4 red RGBA: 16 pixels × 4 channels = 64 bytes
187        let data: Vec<u8> = (0..64)
188            .map(|i| match i % 4 {
189                0 | 3 => 255,
190                _ => 0,
191            })
192            .collect();
193        let result = upload_rgba(&data, 4, 4, "test-canvas");
194        assert!(result.is_ok(), "native stub must return Ok: {result:?}");
195    }
196}