1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
//! Raster export in the pixel layouts other libraries consume.
//!
//! [`Figure::render`] yields tiny-skia's premultiplied RGBA. Windows,
//! terminal-graphics protocols, GUI textures and image crates each want their
//! own layout, so [`Figure::to_rgba8`], [`Figure::to_rgb8`],
//! [`Figure::render_into`] and [`Figure::render_into_u32`] convert once, into a
//! fresh or caller-owned buffer.
//!
//! The opaque layouts ([`PixelFormat::Rgb8`], [`PixelFormat::Bgrx8`],
//! [`PixelFormat::Xrgb8888`] and `render_into_u32`) composite any transparency
//! over the figure's face color made opaque, as matplotlib does when saving a
//! format without alpha.
use crate::core::color::Rgba;
use crate::figure::Figure;
/// A pixel layout for [`Figure::render_into`]. Rows run top to bottom, pixels
/// left to right, with no padding between rows.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PixelFormat {
/// 3 bytes per pixel, `R G B`, composited over the face color — for the
/// `image` crate's `RgbImage`, PDF embedding, BMP and terminal graphics.
Rgb8,
/// 4 bytes per pixel, `R G B A` with straight alpha — the bytes a PNG
/// stores, and what browser `ImageData` and most GPU textures expect.
Rgba8,
/// 4 bytes per pixel, `R G B A` with premultiplied alpha — tiny-skia's own
/// layout, copied without conversion.
Rgba8Premul,
/// 4 bytes per pixel, `B G R X` with `X = 255`, composited over the face
/// color — plotters' `BGRXPixel`, Windows DIBs and many framebuffers.
Bgrx8,
/// 4 bytes per pixel, each a native-endian `u32` holding `0x00RRGGBB`,
/// composited over the face color — a `u32` framebuffer (minifb,
/// softbuffer) viewed as bytes. With a `&mut [u32]` buffer, use
/// [`Figure::render_into_u32`] instead.
Xrgb8888,
}
impl PixelFormat {
/// Bytes each pixel occupies in this layout.
#[must_use]
pub const fn bytes_per_pixel(self) -> usize {
match self {
PixelFormat::Rgb8 => 3,
PixelFormat::Rgba8
| PixelFormat::Rgba8Premul
| PixelFormat::Bgrx8
| PixelFormat::Xrgb8888 => 4,
}
}
}
/// A caller-supplied buffer whose length does not match the raster:
/// `expected` is `width * height * bytes_per_pixel` elements.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BufferSizeError {
/// The length the raster needs.
pub expected: usize,
/// The length supplied.
pub actual: usize,
}
impl std::fmt::Display for BufferSizeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"raster buffer holds {} elements but the figure needs {}",
self.actual, self.expected
)
}
}
impl std::error::Error for BufferSizeError {}
impl Figure {
/// The `(width, height)` in pixels of the raster [`render`](Figure::render)
/// and the methods below produce.
#[must_use]
pub fn raster_size(&self) -> (u32, u32) {
let (w, h) = self.size_px();
(w as u32, h as u32)
}
/// Render to straight-alpha RGBA bytes, returning `(pixels, width,
/// height)`: the same bytes PNG export stores, without encoding them.
///
/// ```
/// use rizzma::Figure;
/// let mut fig = Figure::new(2.0, 1.5);
/// fig.add_subplot(1, 1, 1).plot(&[0.0, 1.0], &[0.0, 1.0]);
/// let (rgba, w, h) = fig.to_rgba8();
/// assert_eq!(rgba.len(), (w * h * 4) as usize);
/// // A Slint texture: SharedPixelBuffer::<Rgba8Pixel>::clone_from_slice(&rgba, w, h)
/// // A wasm canvas: ImageData::new_with_u8_clamped_array_and_sh(Clamped(&rgba), w, h)
/// ```
#[must_use]
pub fn to_rgba8(&self) -> (Vec<u8>, u32, u32) {
self.to_buffer(PixelFormat::Rgba8)
}
/// Render to RGB bytes composited over the face color, returning
/// `(pixels, width, height)`.
///
/// ```
/// use rizzma::Figure;
/// let mut fig = Figure::new(2.0, 1.5);
/// fig.add_subplot(1, 1, 1).plot(&[0.0, 1.0], &[0.0, 1.0]);
/// let (rgb, w, h) = fig.to_rgb8();
/// assert_eq!(rgb.len(), (w * h * 3) as usize);
/// // The image crate: image::RgbImage::from_raw(w, h, rgb)
/// // -> DynamicImage for kitty / ratatui terminal graphics or PDF embedding.
/// ```
#[must_use]
pub fn to_rgb8(&self) -> (Vec<u8>, u32, u32) {
self.to_buffer(PixelFormat::Rgb8)
}
/// Render into a caller-owned byte buffer in `format`. The buffer must
/// hold exactly `width * height * format.bytes_per_pixel()` bytes, with
/// `(width, height)` from [`raster_size`](Figure::raster_size); a buffer of
/// any other length is left untouched and reported as an error.
///
/// # Errors
///
/// Returns [`BufferSizeError`] when `buf` has the wrong length.
///
/// ```
/// use rizzma::{Figure, PixelFormat};
/// let mut fig = Figure::new(2.0, 1.5);
/// fig.add_subplot(1, 1, 1).plot(&[0.0, 1.0], &[0.0, 1.0]);
/// let (w, h) = fig.raster_size();
///
/// // A BGRX framebuffer, as plotters' BitMapBackend::<BGRXPixel> drew into.
/// let mut bgrx = vec![0u8; (w * h) as usize * PixelFormat::Bgrx8.bytes_per_pixel()];
/// fig.render_into(&mut bgrx, PixelFormat::Bgrx8)?;
///
/// // RGB for a BMP data URL in a wasm client.
/// let mut rgb = vec![0u8; (w * h * 3) as usize];
/// fig.render_into(&mut rgb, PixelFormat::Rgb8)?;
///
/// // A wrong-sized buffer is an error, not a panic.
/// assert!(fig.render_into(&mut rgb[1..], PixelFormat::Rgb8).is_err());
/// # Ok::<(), rizzma::BufferSizeError>(())
/// ```
pub fn render_into(&self, buf: &mut [u8], format: PixelFormat) -> Result<(), BufferSizeError> {
let (w, h) = self.raster_size();
let expected = w as usize * h as usize * format.bytes_per_pixel();
if buf.len() != expected {
return Err(BufferSizeError {
expected,
actual: buf.len(),
});
}
let renderer = self.render();
let background = opaque(self.facecolor());
let pixels = renderer.pixmap().pixels();
let chunks = buf.chunks_exact_mut(format.bytes_per_pixel());
for (out, &p) in chunks.zip(pixels) {
match format {
PixelFormat::Rgba8 => {
let c = p.demultiply();
out.copy_from_slice(&[c.red(), c.green(), c.blue(), c.alpha()]);
}
PixelFormat::Rgba8Premul => {
out.copy_from_slice(&[p.red(), p.green(), p.blue(), p.alpha()]);
}
PixelFormat::Rgb8 => out.copy_from_slice(&over(p, background)),
PixelFormat::Bgrx8 => {
let [r, g, b] = over(p, background);
out.copy_from_slice(&[b, g, r, 255]);
}
PixelFormat::Xrgb8888 => {
out.copy_from_slice(&xrgb(over(p, background)).to_ne_bytes());
}
}
}
Ok(())
}
/// Render into a caller-owned `u32` framebuffer, one `0x00RRGGBB` pixel per
/// element, composited over the face color — the layout minifb and
/// softbuffer windows take. The buffer must hold exactly `width * height`
/// elements.
///
/// # Errors
///
/// Returns [`BufferSizeError`] when `buf` has the wrong length.
///
/// ```
/// use rizzma::Figure;
/// let mut fig = Figure::new(2.0, 1.5);
/// fig.add_subplot(1, 1, 1).plot(&[0.0, 1.0], &[0.0, 1.0]);
/// let (w, h) = fig.raster_size();
/// let mut frame = vec![0u32; (w * h) as usize];
/// fig.render_into_u32(&mut frame)?;
/// // window.update_with_buffer(&frame, w as usize, h as usize) (minifb)
/// # Ok::<(), rizzma::BufferSizeError>(())
/// ```
pub fn render_into_u32(&self, buf: &mut [u32]) -> Result<(), BufferSizeError> {
let (w, h) = self.raster_size();
let expected = w as usize * h as usize;
if buf.len() != expected {
return Err(BufferSizeError {
expected,
actual: buf.len(),
});
}
let renderer = self.render();
let background = opaque(self.facecolor());
for (out, &p) in buf.iter_mut().zip(renderer.pixmap().pixels()) {
*out = xrgb(over(p, background));
}
Ok(())
}
/// A fresh buffer in `format`, with the raster size.
fn to_buffer(&self, format: PixelFormat) -> (Vec<u8>, u32, u32) {
let (w, h) = self.raster_size();
let mut buf = vec![0; w as usize * h as usize * format.bytes_per_pixel()];
self.render_into(&mut buf, format)
.expect("buffer sized from raster_size");
(buf, w, h)
}
}
/// `color` as opaque 8-bit RGB, alpha discarded.
fn opaque(color: Rgba) -> [u8; 3] {
let [r, g, b, _] = color.to_u8_array();
[r, g, b]
}
/// The premultiplied pixel `p` composited over the opaque `background`.
fn over(p: tiny_skia::PremultipliedColorU8, background: [u8; 3]) -> [u8; 3] {
let rest = 255 - u16::from(p.alpha());
let blend = |c: u8, bg: u8| {
let v = u16::from(c) + (rest * u16::from(bg) + 127) / 255;
v.min(255) as u8
};
[
blend(p.red(), background[0]),
blend(p.green(), background[1]),
blend(p.blue(), background[2]),
]
}
/// `[r, g, b]` packed as `0x00RRGGBB`.
fn xrgb([r, g, b]: [u8; 3]) -> u32 {
u32::from(r) << 16 | u32::from(g) << 8 | u32::from(b)
}