oxiui_render_soft/headless.rs
1/// Output pixel format for [`crate::backend::SoftBackend::to_bytes`].
2///
3/// Selects the byte layout when exporting the framebuffer's `0xAARRGGBB`
4/// native format to a flat byte slice.
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub enum PixelFormat {
7 /// 4 bytes per pixel in memory order A, R, G, B
8 /// (direct dump of the native `0xAARRGGBB` u32, big-endian byte order).
9 Argb32,
10 /// 4 bytes per pixel in memory order B, G, R, A
11 /// (Windows DIB / DirectX BGRA convention).
12 Bgra8,
13 /// 2 bytes per pixel: 5 red + 6 green + 5 blue, big-endian.
14 Rgb565,
15}
16
17/// RGBA pixel buffer returned by headless rendering.
18///
19/// Pixels are stored row-major, left-to-right, top-to-bottom.
20/// Each pixel occupies exactly 4 bytes in the order R, G, B, A.
21pub struct RgbaBuffer {
22 /// Width in pixels.
23 pub width: u32,
24 /// Height in pixels.
25 pub height: u32,
26 /// Raw pixel data: `width * height * 4` bytes (R, G, B, A per pixel).
27 pub data: Vec<u8>,
28}
29
30impl RgbaBuffer {
31 /// Returns `true` if any byte in the pixel data is non-zero.
32 ///
33 /// A buffer composed entirely of zero bytes represents a fully transparent
34 /// (or un-rendered) frame. Any non-zero R, G, B, or A value means that at
35 /// least one pixel carried visible or semi-transparent content.
36 pub fn has_content(&self) -> bool {
37 self.data.iter().any(|&b| b != 0)
38 }
39
40 /// Save this buffer as a PNG file at `path`.
41 ///
42 /// Uses the `png` crate (pure Rust) — no C/C++ dependencies.
43 ///
44 /// # Errors
45 /// Returns [`crate::SoftRenderError::Io`] if the file cannot be created or
46 /// written, and [`crate::SoftRenderError::Png`] if the PNG encoder fails.
47 pub fn save_png(&self, path: &std::path::Path) -> Result<(), crate::SoftRenderError> {
48 let file =
49 std::fs::File::create(path).map_err(|e| crate::SoftRenderError::Io(e.to_string()))?;
50 let buf_writer = std::io::BufWriter::new(file);
51 let mut encoder = png::Encoder::new(buf_writer, self.width, self.height);
52 encoder.set_color(png::ColorType::Rgba);
53 encoder.set_depth(png::BitDepth::Eight);
54 let mut writer = encoder
55 .write_header()
56 .map_err(|e| crate::SoftRenderError::Png(e.to_string()))?;
57 writer
58 .write_image_data(&self.data)
59 .map_err(|e| crate::SoftRenderError::Png(e.to_string()))
60 }
61}
62
63/// Background fill colour used by [`render_headless_once`].
64///
65/// This matches the COOLJAPAN default dark theme background (#1A1B26, Tokyo Night).
66/// Using a non-zero fill guarantees that [`RgbaBuffer::has_content`] returns `true`.
67pub const HEADLESS_BG_COLOR: [u8; 4] = [26, 27, 38, 255]; // R, G, B, A
68
69/// Render a minimal UI scene headlessly and return an RGBA pixel buffer.
70///
71/// No window, GPU, or display connection is required. The buffer is filled with
72/// the COOLJAPAN default theme's background colour so that the result is
73/// immediately non-trivial (i.e. [`RgbaBuffer::has_content`] returns `true`).
74///
75/// This function is primarily useful for CI smoke tests and ffi-audit containers
76/// where a display is unavailable.
77///
78/// # Example
79/// ```rust
80/// let buf = oxiui_render_soft::headless::render_headless_once(64, 48);
81/// assert_eq!(buf.width, 64);
82/// assert_eq!(buf.height, 48);
83/// assert!(buf.has_content());
84/// ```
85pub fn render_headless_once(width: u32, height: u32) -> RgbaBuffer {
86 let bg = HEADLESS_BG_COLOR;
87 let data: Vec<u8> = (0..width * height)
88 .flat_map(|_| bg.iter().copied())
89 .collect();
90 RgbaBuffer {
91 width,
92 height,
93 data,
94 }
95}
96
97/// Render a custom scene headlessly and return an RGBA pixel buffer.
98///
99/// The framebuffer is pre-filled with the COOLJAPAN default theme background;
100/// `draw_fn` receives a clipped [`Canvas`](crate::Canvas) to paint the scene.
101/// No window, GPU, or display connection is required — suitable for CI smoke
102/// tests, ffi-audit containers, and screenshot generation.
103///
104/// # Example
105/// ```rust
106/// use oxiui_core::Color;
107/// let buf = oxiui_render_soft::headless::render_headless_scene(64, 48, |c| {
108/// c.fill_rect(8.0, 8.0, 16.0, 16.0, Color(255, 0, 0, 255));
109/// });
110/// assert_eq!(buf.width, 64);
111/// assert!(buf.has_content());
112/// ```
113pub fn render_headless_scene<F>(width: u32, height: u32, draw_fn: F) -> RgbaBuffer
114where
115 F: FnOnce(&mut crate::Canvas<'_>),
116{
117 let bg = HEADLESS_BG_COLOR;
118 let mut fb =
119 crate::Framebuffer::with_fill(width, height, oxiui_core::Color(bg[0], bg[1], bg[2], bg[3]));
120 {
121 let mut canvas = crate::Canvas::new(&mut fb);
122 draw_fn(&mut canvas);
123 }
124 fb.to_rgba_buffer()
125}