use tiny_skia::{FilterQuality, Pixmap, PixmapPaint, Transform};
use zenith_core::{AssetProvider, FontProvider};
use zenith_scene::{
BlendMode as IrBlendMode, FilterSpec, MaskSpec, Scene, SceneCommand, ShadowSpec,
};
use super::commands::{DrawCtx, draw_command};
use super::filter::apply_filters;
use super::mask::attenuate_by_mask;
use super::paths::intersect_rects;
use super::pixels::{f64_to_px, premultiplied_to_straight};
use super::shadow::{composite_shadows, gaussian_blur_premul};
use crate::backend::{RasterBackend, RasterImage};
use crate::error::RenderError;
pub struct TinySkiaBackend;
fn map_blend_mode(b: Option<IrBlendMode>) -> tiny_skia::BlendMode {
use tiny_skia::BlendMode as Tk;
match b {
None | Some(IrBlendMode::Normal) => Tk::SourceOver,
Some(IrBlendMode::Multiply) => Tk::Multiply,
Some(IrBlendMode::Screen) => Tk::Screen,
Some(IrBlendMode::Overlay) => Tk::Overlay,
Some(IrBlendMode::Darken) => Tk::Darken,
Some(IrBlendMode::Lighten) => Tk::Lighten,
Some(IrBlendMode::ColorDodge) => Tk::ColorDodge,
Some(IrBlendMode::ColorBurn) => Tk::ColorBurn,
Some(IrBlendMode::HardLight) => Tk::HardLight,
Some(IrBlendMode::SoftLight) => Tk::SoftLight,
Some(IrBlendMode::Difference) => Tk::Difference,
Some(IrBlendMode::Exclusion) => Tk::Exclusion,
}
}
enum CaptureEffect {
Shadow(Vec<ShadowSpec>),
Blur(f64),
Filter(Vec<FilterSpec>),
Mask(MaskSpec),
}
struct CaptureLayer {
pm: Option<Pixmap>,
effect: CaptureEffect,
}
fn current_target<'a>(
capture_stack: &'a mut [CaptureLayer],
layer_stack: &'a mut [(Pixmap, f32, tiny_skia::BlendMode)],
base: &'a mut Pixmap,
) -> &'a mut Pixmap {
if let Some(layer) = capture_stack.iter_mut().rev().find(|l| l.pm.is_some()) {
if let Some(pm) = layer.pm.as_mut() {
return pm;
}
}
if let Some((pm, _, _)) = layer_stack.last_mut() {
return pm;
}
base
}
impl RasterBackend for TinySkiaBackend {
fn rasterize(
&self,
scene: &Scene,
fonts: &dyn FontProvider,
assets: &dyn AssetProvider,
) -> Result<RasterImage, RenderError> {
let width = f64_to_px(scene.width, "width")?;
let height = f64_to_px(scene.height, "height")?;
let mut pixmap = Pixmap::new(width, height).ok_or_else(|| {
RenderError::new(format!("failed to allocate pixmap ({width}×{height})"))
})?;
let page_clip = (0.0_f64, 0.0_f64, scene.width, scene.height);
let mut clip_stack: Vec<(f64, f64, f64, f64)> = vec![page_clip];
let mut transform_stack: Vec<Transform> = vec![Transform::identity()];
let mut svg_fontdb: Option<resvg::usvg::fontdb::Database> = None;
let mut capture_stack: Vec<CaptureLayer> = Vec::new();
let mut layer_stack: Vec<(Pixmap, f32, tiny_skia::BlendMode)> = Vec::new();
for cmd in &scene.commands {
let current_ts = *transform_stack.last().unwrap_or(&Transform::identity());
match cmd {
SceneCommand::PushClip { x, y, w, h } => {
let new_rect = (*x, *y, x + w, y + h);
let current = *clip_stack.last().unwrap_or(&page_clip);
let intersected =
intersect_rects(current, new_rect).unwrap_or((0.0, 0.0, 0.0, 0.0)); clip_stack.push(intersected);
continue;
}
SceneCommand::PopClip => {
if clip_stack.len() > 1 {
clip_stack.pop();
}
continue;
}
SceneCommand::PushTransform { angle_deg, cx, cy } => {
let rot = Transform::from_rotate_at(*angle_deg as f32, *cx as f32, *cy as f32);
transform_stack.push(current_ts.pre_concat(rot));
continue;
}
SceneCommand::PopTransform => {
if transform_stack.len() > 1 {
transform_stack.pop();
}
continue;
}
SceneCommand::BeginShadow { shadows } => {
let pm = Pixmap::new(width, height);
capture_stack.push(CaptureLayer {
pm,
effect: CaptureEffect::Shadow(shadows.clone()),
});
continue;
}
SceneCommand::EndShadow => {
if let Some(layer) = capture_stack.pop()
&& let (Some(ink), CaptureEffect::Shadow(shadows)) =
(layer.pm, layer.effect)
{
let shadow_target =
current_target(&mut capture_stack, &mut layer_stack, &mut pixmap);
composite_shadows(shadow_target, &ink, &shadows, width, height);
}
continue;
}
SceneCommand::BeginBlur { radius } => {
let pm = Pixmap::new(width, height);
capture_stack.push(CaptureLayer {
pm,
effect: CaptureEffect::Blur(*radius),
});
continue;
}
SceneCommand::EndBlur => {
if let Some(layer) = capture_stack.pop()
&& let (Some(mut ink), CaptureEffect::Blur(sigma)) =
(layer.pm, layer.effect)
{
gaussian_blur_premul(&mut ink, sigma);
let blur_target =
current_target(&mut capture_stack, &mut layer_stack, &mut pixmap);
blur_target.draw_pixmap(
0,
0,
ink.as_ref(),
&PixmapPaint::default(),
Transform::identity(),
None,
);
}
continue;
}
SceneCommand::BeginFilter { filters } => {
let pm = if filters.is_empty() {
None
} else {
Pixmap::new(width, height)
};
capture_stack.push(CaptureLayer {
pm,
effect: CaptureEffect::Filter(filters.clone()),
});
continue;
}
SceneCommand::EndFilter => {
if let Some(layer) = capture_stack.pop()
&& let (Some(mut ink), CaptureEffect::Filter(filters)) =
(layer.pm, layer.effect)
{
apply_filters(&mut ink, &filters);
let filter_target =
current_target(&mut capture_stack, &mut layer_stack, &mut pixmap);
filter_target.draw_pixmap(
0,
0,
ink.as_ref(),
&PixmapPaint::default(),
Transform::identity(),
None,
);
}
continue;
}
SceneCommand::BeginMask { mask } => {
let pm = Pixmap::new(width, height);
capture_stack.push(CaptureLayer {
pm,
effect: CaptureEffect::Mask(*mask),
});
continue;
}
SceneCommand::EndMask => {
if let Some(layer) = capture_stack.pop()
&& let (Some(mut ink), CaptureEffect::Mask(spec)) = (layer.pm, layer.effect)
{
attenuate_by_mask(&mut ink, &spec);
let target =
current_target(&mut capture_stack, &mut layer_stack, &mut pixmap);
target.draw_pixmap(
0,
0,
ink.as_ref(),
&PixmapPaint::default(),
Transform::identity(),
None,
);
}
continue;
}
SceneCommand::PushLayer {
opacity,
blend_mode,
} => {
if let Some(pm) = Pixmap::new(width, height) {
layer_stack.push((pm, *opacity as f32, map_blend_mode(*blend_mode)));
}
continue;
}
SceneCommand::PopLayer => {
if let Some((layer_pm, op, bm)) = layer_stack.pop() {
let target_after_pop =
current_target(&mut capture_stack, &mut layer_stack, &mut pixmap);
target_after_pop.draw_pixmap(
0,
0,
layer_pm.as_ref(),
&PixmapPaint {
opacity: op.clamp(0.0, 1.0),
blend_mode: bm,
quality: FilterQuality::Nearest,
},
Transform::identity(),
None,
);
}
continue;
}
SceneCommand::FillRect { .. }
| SceneCommand::StrokeRect { .. }
| SceneCommand::FillRoundedRect { .. }
| SceneCommand::StrokeRoundedRect { .. }
| SceneCommand::FillEllipse { .. }
| SceneCommand::StrokeEllipse { .. }
| SceneCommand::StrokeLine { .. }
| SceneCommand::FillPolygon { .. }
| SceneCommand::StrokePolyline { .. }
| SceneCommand::DrawImage { .. }
| SceneCommand::DrawSvgAsset { .. }
| SceneCommand::DrawGlyphRun { .. } => {}
}
let target: &mut Pixmap =
current_target(&mut capture_stack, &mut layer_stack, &mut pixmap);
let ctx = DrawCtx {
current_ts,
effective_clip: *clip_stack.last().unwrap_or(&page_clip),
width,
height,
};
draw_command(target, ctx, cmd, fonts, assets, &mut svg_fontdb);
}
let raw = pixmap.data(); let mut rgba = Vec::with_capacity(raw.len());
for chunk in raw.chunks_exact(4) {
let (sr, sg, sb, sa) =
premultiplied_to_straight(chunk[0], chunk[1], chunk[2], chunk[3]);
rgba.push(sr);
rgba.push(sg);
rgba.push(sb);
rgba.push(sa);
}
Ok(RasterImage {
width,
height,
rgba,
})
}
fn encode_png(&self, image: &RasterImage) -> Result<Vec<u8>, RenderError> {
let mut premul = Vec::with_capacity(image.rgba.len());
for chunk in image.rgba.chunks_exact(4) {
let (r, g, b, a) = (chunk[0], chunk[1], chunk[2], chunk[3]);
if a == 0 {
premul.extend_from_slice(&[0, 0, 0, 0]);
} else {
let a_u16 = u16::from(a);
let mul = |v: u8| -> u8 {
let result = (u16::from(v) * a_u16 + 127) / 255;
result.min(255) as u8
};
premul.push(mul(r));
premul.push(mul(g));
premul.push(mul(b));
premul.push(a);
}
}
let mut pixmap = Pixmap::new(image.width, image.height).ok_or_else(|| {
RenderError::new(format!(
"failed to allocate pixmap for encoding ({}×{})",
image.width, image.height
))
})?;
let dst = pixmap.data_mut();
if dst.len() != premul.len() {
return Err(RenderError::new(
"pixel buffer length mismatch during PNG encoding",
));
}
dst.copy_from_slice(&premul);
pixmap
.encode_png()
.map_err(|e| RenderError::new(format!("PNG encoding failed: {e}")))
}
}