use paint_cat::PaintCommand;
use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, Rect, Stroke, Transform};
use crate::frame::Frame;
use crate::text::TextRenderer;
#[derive(Debug, Clone)]
pub struct PixelBuffer {
width: u32,
height: u32,
bytes: Vec<u8>,
}
impl PixelBuffer {
#[must_use]
pub fn width(&self) -> u32 {
self.width
}
#[must_use]
pub fn height(&self) -> u32 {
self.height
}
#[must_use]
pub fn rgba(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn pixel(&self, x: u32, y: u32, channel: usize) -> Option<u8> {
if x >= self.width || y >= self.height || channel >= 4 {
None
} else {
let row_bytes = usize::try_from(self.width)
.ok()
.and_then(|w| w.checked_mul(4))?;
let row_index = usize::try_from(y).ok()?.checked_mul(row_bytes)?;
let col_index = usize::try_from(x).ok().and_then(|c| c.checked_mul(4))?;
self.bytes.get(row_index + col_index + channel).copied()
}
}
}
#[must_use]
pub fn render_to_pixels(frame: &Frame, width: u32, height: u32) -> PixelBuffer {
let mut text_renderer = TextRenderer::new();
render_to_pixels_with(frame, width, height, &mut text_renderer)
}
#[must_use]
pub fn render_to_pixels_with(
frame: &Frame,
width: u32,
height: u32,
text_renderer: &mut TextRenderer,
) -> PixelBuffer {
let bytes = rasterize_to_bytes(frame, width, height, text_renderer);
PixelBuffer {
width,
height,
bytes,
}
}
fn rasterize_to_bytes(
frame: &Frame,
width: u32,
height: u32,
text_renderer: &mut TextRenderer,
) -> Vec<u8> {
Pixmap::new(width, height).map_or_else(
|| empty_bytes(width, height),
|pixmap| paint_commands(pixmap, frame, text_renderer),
)
}
fn empty_bytes(width: u32, height: u32) -> Vec<u8> {
let total = usize::try_from(width)
.ok()
.and_then(|w| {
usize::try_from(height)
.ok()
.and_then(|h| w.checked_mul(h).and_then(|p| p.checked_mul(4)))
})
.unwrap_or(0);
vec![0; total]
}
fn paint_commands(pixmap_in: Pixmap, frame: &Frame, text_renderer: &mut TextRenderer) -> Vec<u8> {
let mut pixmap = pixmap_in;
pixmap.fill(tiny_skia::Color::TRANSPARENT);
frame.display_list().commands().iter().for_each(|cmd| {
apply_command(&mut pixmap, cmd, text_renderer);
});
pixmap.take()
}
fn apply_command(pixmap: &mut Pixmap, command: &PaintCommand, text_renderer: &mut TextRenderer) {
match command {
PaintCommand::FillRect { rect, color } => fill_rect(pixmap, rect, color),
PaintCommand::StrokeRect { rect, color, width } => stroke_rect(pixmap, rect, color, *width),
PaintCommand::FillText {
rect,
text,
color,
font_size,
} => text_renderer.render_text(pixmap, *rect, text, *color, *font_size),
}
}
fn fill_rect(pixmap: &mut Pixmap, rect: &layout_cat::Rect, color: &layout_cat::Color) {
if let Some(skia_rect) = rect_to_skia(rect) {
let paint = build_paint(color);
let path = PathBuilder::from_rect(skia_rect);
pixmap.fill_path(
&path,
&paint,
FillRule::Winding,
Transform::identity(),
None,
);
}
}
fn stroke_rect(
pixmap: &mut Pixmap,
rect: &layout_cat::Rect,
color: &layout_cat::Color,
width: f64,
) {
if let Some(skia_rect) = rect_to_skia(rect) {
let paint = build_paint(color);
let path = PathBuilder::from_rect(skia_rect);
let stroke = Stroke {
width: f32_from_f64(width).max(0.0),
..Stroke::default()
};
pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
}
fn rect_to_skia(rect: &layout_cat::Rect) -> Option<Rect> {
let x = f32_from_f64(rect.origin().x());
let y = f32_from_f64(rect.origin().y());
let w = f32_from_f64(rect.width()).max(0.0);
let h = f32_from_f64(rect.height()).max(0.0);
if w == 0.0 || h == 0.0 {
None
} else {
Rect::from_xywh(x, y, w, h)
}
}
fn build_paint(color: &layout_cat::Color) -> Paint<'static> {
let r = f32_from_f64(color.red()).clamp(0.0, 1.0);
let g = f32_from_f64(color.green()).clamp(0.0, 1.0);
let b = f32_from_f64(color.blue()).clamp(0.0, 1.0);
let a = f32_from_f64(color.alpha()).clamp(0.0, 1.0);
let skia_color =
tiny_skia::Color::from_rgba(r, g, b, a).unwrap_or(tiny_skia::Color::TRANSPARENT);
let mut paint = Paint::default();
paint.set_color(skia_color);
paint.anti_alias = true;
paint
}
fn f32_from_f64(value: f64) -> f32 {
if value.is_finite() {
#[allow(clippy::cast_possible_truncation)]
let n = value as f32;
n
} else if value > 0.0 {
f32::INFINITY
} else if value < 0.0 {
f32::NEG_INFINITY
} else {
0.0
}
}