use anyhow::{anyhow, Result};
use fission_render::{
surface_placeholder_color, Color as RenderColor, DisplayList, DisplayOp, Fill, ImageFit,
LineCap, LineJoin, RenderScene, Stroke, TextRun,
};
use fontdue::{
layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle as FontdueTextStyle},
Font, FontSettings,
};
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::fs;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex, OnceLock};
use tiny_skia::{
Color, FillRule as TinyFillRule, FilterQuality, GradientStop, LineCap as TinyLineCap,
LineJoin as TinyLineJoin, Mask, Paint, Path, PathBuilder, Pixmap, PixmapPaint, Point,
PremultipliedColorU8, Shader, SpreadMode, Stroke as TinyStroke, Transform,
};
use vello::kurbo::{BezPath, PathEl, Rect as KurboRect, RoundedRect, Shape};
#[derive(Clone)]
struct DrawState {
transform: Transform,
clip: Option<Mask>,
surface: usize,
layer_alpha: Option<f32>,
}
#[derive(Debug, Clone)]
struct SvgCacheEntry {
view_box: Option<(f32, f32, f32, f32)>,
shapes: Vec<SvgShape>,
}
#[derive(Debug, Clone)]
enum SvgShape {
Path(BezPath),
Rect {
x: f32,
y: f32,
width: f32,
height: f32,
},
}
static DEFAULT_FONT: OnceLock<Font> = OnceLock::new();
static IMAGE_CACHE: OnceLock<Mutex<HashMap<String, Arc<Pixmap>>>> = OnceLock::new();
static SVG_CACHE: OnceLock<Mutex<HashMap<u64, Arc<SvgCacheEntry>>>> = OnceLock::new();
fn default_font() -> &'static Font {
DEFAULT_FONT.get_or_init(|| {
Font::from_bytes(
fission_theme::fonts::default_font_bytes(),
FontSettings::default(),
)
.expect("failed to load bundled UI font")
})
}
fn image_cache() -> &'static Mutex<HashMap<String, Arc<Pixmap>>> {
IMAGE_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn svg_cache() -> &'static Mutex<HashMap<u64, Arc<SvgCacheEntry>>> {
SVG_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn rgba_to_premul(color: RenderColor, coverage: u8) -> PremultipliedColorU8 {
let alpha = ((u16::from(color.a) * u16::from(coverage)) / 255) as u8;
PremultipliedColorU8::from_rgba(
((u16::from(color.r) * u16::from(alpha)) / 255) as u8,
((u16::from(color.g) * u16::from(alpha)) / 255) as u8,
((u16::from(color.b) * u16::from(alpha)) / 255) as u8,
alpha,
)
.unwrap_or(PremultipliedColorU8::TRANSPARENT)
}
fn tiny_color(color: RenderColor) -> Color {
Color::from_rgba8(color.r, color.g, color.b, color.a)
}
fn fill_shader(fill: &Fill) -> Option<Shader<'static>> {
match fill {
Fill::Solid(color) => Some(Shader::SolidColor(tiny_color(*color))),
Fill::LinearGradient { start, end, stops } => {
let stops = stops
.iter()
.map(|(offset, color)| GradientStop::new(*offset, tiny_color(*color)))
.collect::<Vec<_>>();
tiny_skia::LinearGradient::new(
Point::from_xy(start.0, start.1),
Point::from_xy(end.0, end.1),
stops,
SpreadMode::Pad,
Transform::identity(),
)
}
Fill::RadialGradient {
center,
radius,
stops,
} => {
let stops = stops
.iter()
.map(|(offset, color)| GradientStop::new(*offset, tiny_color(*color)))
.collect::<Vec<_>>();
tiny_skia::RadialGradient::new(
Point::from_xy(center.0, center.1),
Point::from_xy(center.0, center.1),
*radius,
stops,
SpreadMode::Pad,
Transform::identity(),
)
}
}
}
fn fill_paint(fill: &Fill) -> Paint<'static> {
let mut paint = Paint::default();
if let Some(shader) = fill_shader(fill) {
paint.shader = shader;
}
paint.anti_alias = true;
paint
}
fn stroke_style(stroke: &Stroke) -> TinyStroke {
let mut style = TinyStroke::default();
style.width = stroke.width;
style.line_cap = match stroke.line_cap {
LineCap::Butt => TinyLineCap::Butt,
LineCap::Round => TinyLineCap::Round,
LineCap::Square => TinyLineCap::Square,
};
style.line_join = match stroke.line_join {
LineJoin::Miter => TinyLineJoin::Miter,
LineJoin::Round => TinyLineJoin::Round,
LineJoin::Bevel => TinyLineJoin::Bevel,
};
if let Some(dash_array) = &stroke.dash_array {
style.dash = tiny_skia::StrokeDash::new(dash_array.clone(), 0.0);
}
style
}
fn rounded_rect_path(rect: fission_render::LayoutRect, radius: f32) -> Option<Path> {
let rounded = RoundedRect::from_rect(
KurboRect::new(
rect.origin.x as f64,
rect.origin.y as f64,
rect.right() as f64,
rect.bottom() as f64,
),
radius as f64,
);
bez_to_tiny_path(&rounded.to_path(0.1))
}
fn rect_path(rect: fission_render::LayoutRect) -> Option<Path> {
let bez = KurboRect::new(
rect.origin.x as f64,
rect.origin.y as f64,
rect.right() as f64,
rect.bottom() as f64,
)
.to_path(0.1);
bez_to_tiny_path(&bez)
}
fn bez_to_tiny_path(path: &BezPath) -> Option<Path> {
let mut builder = PathBuilder::new();
for el in path.elements() {
match el {
PathEl::MoveTo(p) => builder.move_to(p.x as f32, p.y as f32),
PathEl::LineTo(p) => builder.line_to(p.x as f32, p.y as f32),
PathEl::QuadTo(p1, p2) => {
builder.quad_to(p1.x as f32, p1.y as f32, p2.x as f32, p2.y as f32)
}
PathEl::CurveTo(p1, p2, p3) => builder.cubic_to(
p1.x as f32,
p1.y as f32,
p2.x as f32,
p2.y as f32,
p3.x as f32,
p3.y as f32,
),
PathEl::ClosePath => builder.close(),
}
}
builder.finish()
}
fn svg_cache_key(content: &str) -> u64 {
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
hasher.finish()
}
fn parse_svg_entry(content: &str) -> SvgCacheEntry {
let parse_view_box = |data: &str| -> Option<(f32, f32, f32, f32)> {
let key = "viewBox=\"";
let start = data.find(key)?;
let rest = &data[start + key.len()..];
let end = rest.find('"')?;
let nums: Vec<f32> = rest[..end]
.split(|c: char| c.is_whitespace() || c == ',')
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse().ok())
.collect();
(nums.len() == 4).then_some((nums[0], nums[1], nums[2], nums[3]))
};
let mut shapes = Vec::new();
for tag in content.split('<').skip(1) {
let tag = tag.split('>').next().unwrap_or("");
let tag_name = tag.split_whitespace().next().unwrap_or("");
if tag_name == "path" {
if let Some(start) = tag.find("d=\"") {
let after = &tag[start + 3..];
if let Some(end) = after.find('"') {
let mut d = after[..end].to_string();
d = d.replace("M0 0h24v24H0z", "");
d = d.replace("M0 0h24v24H0V0z", "");
d = d.replace("M0,0h24v24H0V0z", "");
if !d.trim().is_empty() {
if let Ok(path) = BezPath::from_svg(&d) {
shapes.push(SvgShape::Path(path));
}
}
}
}
} else if tag_name == "rect" {
if tag.contains("fill=\"none\"") || tag.contains("fill='none'") {
continue;
}
let parse_attr = |name: &str| -> f32 {
if let Some(pos) = tag.find(&format!("{}=\"", name)) {
let after = &tag[pos + name.len() + 2..];
if let Some(end) = after.find('"') {
return after[..end].parse().unwrap_or(0.0);
}
}
0.0
};
let x = parse_attr("x");
let y = parse_attr("y");
let width = parse_attr("width");
let height = parse_attr("height");
if width > 0.0 && height > 0.0 {
shapes.push(SvgShape::Rect {
x,
y,
width,
height,
});
}
} else if tag_name == "polygon" {
if let Some(start) = tag.find("points=\"") {
let after = &tag[start + 8..];
if let Some(end) = after.find('"') {
let nums: Vec<f64> = after[..end]
.split(|c: char| c.is_whitespace() || c == ',')
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse().ok())
.collect();
if nums.len() >= 4 {
let mut bez = BezPath::new();
bez.move_to((nums[0], nums[1]));
for i in (2..nums.len()).step_by(2) {
if i + 1 < nums.len() {
bez.line_to((nums[i], nums[i + 1]));
}
}
bez.close_path();
shapes.push(SvgShape::Path(bez));
}
}
}
}
}
SvgCacheEntry {
view_box: parse_view_box(content),
shapes,
}
}
fn svg_cache_entry(content: &str) -> Arc<SvgCacheEntry> {
let key = svg_cache_key(content);
if let Some(entry) = svg_cache().lock().unwrap().get(&key) {
return Arc::clone(entry);
}
let parsed = Arc::new(parse_svg_entry(content));
let mut cache = svg_cache().lock().unwrap();
cache.entry(key).or_insert_with(|| Arc::clone(&parsed));
parsed
}
fn wrap_max_width(bounds_width: f32, font_size: f32, wrap: bool) -> Option<f32> {
if !wrap || bounds_width <= 0.0 {
return None;
}
Some(bounds_width.ceil() + font_size * 0.5)
}
fn cached_image(path: &str) -> Option<Arc<Pixmap>> {
if let Some(image) = image_cache().lock().unwrap().get(path) {
return Some(Arc::clone(image));
}
let data = fs::read(path).ok()?;
let dyn_img = image::load_from_memory(&data).ok()?;
let rgba = dyn_img.to_rgba8();
let (width, height) = rgba.dimensions();
let size = tiny_skia::IntSize::from_wh(width, height)?;
let pixmap = Pixmap::from_vec(rgba.into_raw(), size)?;
let pixmap = Arc::new(pixmap);
let mut cache = image_cache().lock().unwrap();
cache.insert(path.to_string(), Arc::clone(&pixmap));
Some(pixmap)
}
pub struct SoftwareRenderer {
width: u32,
height: u32,
surfaces: Vec<Pixmap>,
states: Vec<DrawState>,
}
impl SoftwareRenderer {
pub fn new(width: u32, height: u32, background: RenderColor) -> Result<Self> {
let mut root = Pixmap::new(width.max(1), height.max(1))
.ok_or_else(|| anyhow!("failed to allocate software render target"))?;
root.fill(tiny_color(background));
Ok(Self {
width: width.max(1),
height: height.max(1),
surfaces: vec![root],
states: vec![DrawState {
transform: Transform::identity(),
clip: None,
surface: 0,
layer_alpha: None,
}],
})
}
pub fn render(
scene: &RenderScene,
width: u32,
height: u32,
background: RenderColor,
scale_factor: f32,
) -> Result<Vec<u8>> {
let logical_width = ((width.max(1) as f32) / scale_factor.max(1.0)).round() as u32;
let logical_height = ((height.max(1) as f32) / scale_factor.max(1.0)).round() as u32;
let mut renderer = Self::new(logical_width.max(1), logical_height.max(1), background)?;
let display_list = scene.flatten();
renderer.render_ops(&display_list)?;
if scale_factor <= 1.0 {
return Ok(renderer.finish());
}
let logical = renderer.finish_pixmap();
let mut output = Pixmap::new(width.max(1), height.max(1))
.ok_or_else(|| anyhow!("failed to allocate upscaled software render target"))?;
output.fill(tiny_color(background));
let mut paint = PixmapPaint::default();
paint.quality = FilterQuality::Bilinear;
output.draw_pixmap(
0,
0,
logical.as_ref(),
&paint,
Transform::from_scale(
width.max(1) as f32 / logical.width() as f32,
height.max(1) as f32 / logical.height() as f32,
),
None,
);
Ok(output.take())
}
fn finish(self) -> Vec<u8> {
self.finish_pixmap().take()
}
fn finish_pixmap(self) -> Pixmap {
self.surfaces.into_iter().next().unwrap()
}
fn current_state(&self) -> &DrawState {
self.states
.last()
.expect("software renderer state stack empty")
}
fn current_state_mut(&mut self) -> &mut DrawState {
self.states
.last_mut()
.expect("software renderer state stack empty")
}
fn current_surface_mut(&mut self) -> &mut Pixmap {
let surface = self.current_state().surface;
&mut self.surfaces[surface]
}
fn current_clip(&self) -> Option<&Mask> {
self.current_state().clip.as_ref()
}
fn push_state(&mut self) {
self.states.push(self.current_state().clone());
}
fn pop_state(&mut self) {
if self.states.len() <= 1 {
return;
}
let finished = self.states.pop().unwrap();
let parent_surface = self.current_state().surface;
if let Some(alpha) = finished.layer_alpha {
if finished.surface != parent_surface {
let clip = self.current_clip().cloned();
let (low, high) = if parent_surface < finished.surface {
let (low, high) = self.surfaces.split_at_mut(finished.surface);
(&mut low[parent_surface], &mut high[0])
} else {
let (low, high) = self.surfaces.split_at_mut(parent_surface);
(&mut high[0], &mut low[finished.surface])
};
let mut paint = PixmapPaint::default();
paint.opacity = alpha;
paint.quality = FilterQuality::Bilinear;
low.draw_pixmap(
0,
0,
high.as_ref(),
&paint,
Transform::identity(),
clip.as_ref(),
);
}
}
}
fn ensure_clip_path(&mut self, path: &Path) {
let transform = self.current_state().transform;
let width = self.width;
let height = self.height;
let state = self.current_state_mut();
if let Some(mask) = state.clip.as_mut() {
mask.intersect_path(path, TinyFillRule::Winding, true, transform);
} else {
let mut mask = Mask::new(width, height).unwrap();
mask.fill_path(path, TinyFillRule::Winding, true, transform);
state.clip = Some(mask);
}
}
fn start_opacity_layer(&mut self, alpha: f32) -> Result<()> {
let mut layer = Pixmap::new(self.width, self.height)
.ok_or_else(|| anyhow!("failed to allocate software layer"))?;
layer.fill(Color::from_rgba8(0, 0, 0, 0));
self.surfaces.push(layer);
let surface = self.surfaces.len() - 1;
let state = self.current_state_mut();
state.surface = surface;
state.layer_alpha = Some(alpha.clamp(0.0, 1.0));
Ok(())
}
fn render_ops(&mut self, display_list: &DisplayList) -> Result<()> {
for op in &display_list.ops {
match op {
DisplayOp::Save => self.push_state(),
DisplayOp::Restore => self.pop_state(),
DisplayOp::ClipRect(rect) => {
if let Some(path) = rect_path(*rect) {
self.ensure_clip_path(&path);
}
}
DisplayOp::ClipRoundedRect { rect, radius } => {
if let Some(path) = rounded_rect_path(*rect, *radius) {
self.ensure_clip_path(&path);
}
}
DisplayOp::OpacityLayer { alpha, .. } => {
self.start_opacity_layer(*alpha)?;
}
DisplayOp::Translate(point) => {
let state = self.current_state_mut();
state.transform = state.transform.post_translate(point.x, point.y);
}
DisplayOp::Transform(matrix) => {
let transform = Transform::from_row(
matrix[0], matrix[1], matrix[4], matrix[5], matrix[12], matrix[13],
);
let state = self.current_state_mut();
state.transform = state.transform.post_concat(transform);
}
DisplayOp::CachedScene { list, .. } => self.render_ops(list)?,
DisplayOp::DrawRect {
rect,
fill,
stroke,
corner_radius,
shadow,
..
} => {
self.draw_rect(
*rect,
fill.as_ref(),
stroke.as_ref(),
*corner_radius,
shadow.as_ref(),
)?;
}
DisplayOp::DrawText {
text,
position,
size,
color,
bounds,
underline,
wrap,
..
} => {
self.draw_text(text, *position, *size, *color, *bounds, *wrap, *underline)?;
}
DisplayOp::DrawRichText {
runs,
position,
bounds,
wrap,
..
} => {
self.draw_rich_text(runs, *position, *bounds, *wrap)?;
}
DisplayOp::DrawImage {
rect, source, fit, ..
} => {
self.draw_image(*rect, source, *fit)?;
}
DisplayOp::DrawPath {
path,
fill,
stroke,
bounds,
..
} => {
self.draw_path(path, fill.as_ref(), stroke.as_ref(), *bounds)?;
}
DisplayOp::DrawSvg {
content,
fill,
stroke,
bounds,
..
} => {
self.draw_svg(content, fill.as_ref(), stroke.as_ref(), *bounds)?;
}
DisplayOp::DrawSurface {
rect,
surface_id,
position,
..
} => {
let color = surface_placeholder_color(*surface_id, *position);
self.draw_rect(*rect, Some(&Fill::Solid(color)), None, 0.0, None)?;
}
}
}
Ok(())
}
fn draw_rect(
&mut self,
rect: fission_render::LayoutRect,
fill: Option<&Fill>,
stroke: Option<&Stroke>,
corner_radius: f32,
shadow: Option<&fission_render::BoxShadow>,
) -> Result<()> {
let path = if corner_radius > 0.0 {
rounded_rect_path(rect, corner_radius)
} else {
rect_path(rect)
}
.ok_or_else(|| anyhow!("failed to build rectangle path"))?;
let transform = self.current_state().transform;
let clip = self.current_clip().cloned();
let surface = self.current_surface_mut();
if let Some(shadow) = shadow {
let shadow_rect = fission_render::LayoutRect::new(
rect.origin.x + shadow.offset.0,
rect.origin.y + shadow.offset.1,
rect.size.width,
rect.size.height,
);
if let Some(shadow_path) = if corner_radius > 0.0 {
rounded_rect_path(shadow_rect, corner_radius)
} else {
rect_path(shadow_rect)
} {
let mut paint = Paint::default();
let mut color = shadow.color;
if shadow.blur_radius > 0.0 {
color.a = ((f32::from(color.a) * 0.65).round() as i32).clamp(0, 255) as u8;
}
paint.set_color(tiny_color(color));
surface.fill_path(
&shadow_path,
&paint,
TinyFillRule::Winding,
transform,
clip.as_ref(),
);
}
}
if let Some(fill) = fill {
let paint = fill_paint(fill);
surface.fill_path(
&path,
&paint,
TinyFillRule::Winding,
transform,
clip.as_ref(),
);
}
if let Some(stroke) = stroke {
let paint = fill_paint(&stroke.fill);
let style = stroke_style(stroke);
surface.stroke_path(&path, &paint, &style, transform, clip.as_ref());
}
Ok(())
}
fn draw_text(
&mut self,
text: &str,
position: fission_render::LayoutPoint,
size: f32,
color: RenderColor,
bounds: fission_render::LayoutRect,
wrap: bool,
underline: bool,
) -> Result<()> {
let font = default_font();
let fonts = [font];
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.reset(&LayoutSettings {
x: position.x,
y: position.y,
max_width: wrap_max_width(bounds.width(), size, wrap),
..LayoutSettings::default()
});
layout.append(&fonts, &FontdueTextStyle::new(text, size, 0));
self.draw_glyphs(&layout, |_, _| color)?;
if underline {
self.draw_layout_underlines(&layout, color, size)?;
}
Ok(())
}
fn draw_rich_text(
&mut self,
runs: &[TextRun],
position: fission_render::LayoutPoint,
bounds: fission_render::LayoutRect,
wrap: bool,
) -> Result<()> {
let font = default_font();
let fonts = [font];
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.reset(&LayoutSettings {
x: position.x,
y: position.y,
max_width: wrap_max_width(
bounds.width(),
runs.first().map(|run| run.style.font_size).unwrap_or(14.0),
wrap,
),
..LayoutSettings::default()
});
for run in runs {
layout.append(
&fonts,
&fontdue::layout::TextStyle::with_user_data(
&run.text,
run.style.font_size,
0,
(
run.style.color,
run.style.underline,
run.style.background_color,
),
),
);
}
self.draw_glyphs(&layout, |glyph, (color, _underline, bg)| {
if bg.is_some() {
let _ = glyph;
}
*color
})?;
if let Some(lines) = layout.lines() {
for line in lines {
for glyph in &layout.glyphs()[line.glyph_start..=line.glyph_end] {
let (color, underline, _) = glyph.user_data;
if underline {
let underline_rect = fission_render::LayoutRect::new(
glyph.x,
line.baseline_y + 1.5,
glyph.width as f32,
(glyph.key.px / 14.0).max(1.0),
);
self.draw_rect(underline_rect, Some(&Fill::Solid(color)), None, 0.0, None)?;
}
}
}
}
Ok(())
}
fn draw_layout_underlines<U: Copy + Clone>(
&mut self,
layout: &Layout<U>,
color: RenderColor,
size: f32,
) -> Result<()> {
if let Some(lines) = layout.lines() {
for line in lines {
if line.glyph_start > line.glyph_end || line.glyph_end >= layout.glyphs().len() {
continue;
}
let first = &layout.glyphs()[line.glyph_start];
let last = &layout.glyphs()[line.glyph_end];
let underline_rect = fission_render::LayoutRect::new(
first.x,
line.baseline_y + 1.5,
(last.x + last.width as f32 - first.x).max(1.0),
(size / 14.0).max(1.0),
);
self.draw_rect(underline_rect, Some(&Fill::Solid(color)), None, 0.0, None)?;
}
}
Ok(())
}
fn draw_glyphs<U: Copy + Clone>(
&mut self,
layout: &Layout<U>,
color_for: impl Fn(&fontdue::layout::GlyphPosition<U>, &U) -> RenderColor,
) -> Result<()> {
let font = default_font();
let transform = self.current_state().transform;
let clip = self.current_clip().cloned();
let surface = self.current_surface_mut();
for glyph in layout.glyphs() {
if glyph.width == 0 || glyph.height == 0 {
continue;
}
let color = color_for(glyph, &glyph.user_data);
let (metrics, bitmap) = font.rasterize_indexed(glyph.key.glyph_index, glyph.key.px);
if metrics.width == 0 || metrics.height == 0 || bitmap.is_empty() {
continue;
}
let mut rgba = Vec::with_capacity(metrics.width * metrics.height * 4);
for coverage in bitmap {
let premul = rgba_to_premul(color, coverage);
rgba.extend_from_slice(&[
premul.red(),
premul.green(),
premul.blue(),
premul.alpha(),
]);
}
let size = tiny_skia::IntSize::from_wh(metrics.width as u32, metrics.height as u32)
.ok_or_else(|| anyhow!("invalid glyph pixmap size"))?;
let pixmap = Pixmap::from_vec(rgba, size)
.ok_or_else(|| anyhow!("failed to create glyph pixmap"))?;
surface.draw_pixmap(
glyph.x.round() as i32,
glyph.y.round() as i32,
pixmap.as_ref(),
&PixmapPaint::default(),
transform,
clip.as_ref(),
);
}
Ok(())
}
fn draw_image(
&mut self,
rect: fission_render::LayoutRect,
source: &str,
fit: ImageFit,
) -> Result<()> {
let image = match cached_image(source) {
Some(image) => image,
None => return Ok(()),
};
let rect_w = rect.width();
let rect_h = rect.height();
let img_w = image.width() as f32;
let img_h = image.height() as f32;
if rect_w <= 0.0 || rect_h <= 0.0 || img_w <= 0.0 || img_h <= 0.0 {
return Ok(());
}
let (scale_x, scale_y, dx, dy) = match fit {
ImageFit::Fill => (rect_w / img_w, rect_h / img_h, rect.origin.x, rect.origin.y),
ImageFit::Contain => {
let scale = (rect_w / img_w).min(rect_h / img_h);
let w = img_w * scale;
let h = img_h * scale;
(
scale,
scale,
rect.origin.x + (rect_w - w) / 2.0,
rect.origin.y + (rect_h - h) / 2.0,
)
}
ImageFit::Cover => {
let scale = (rect_w / img_w).max(rect_h / img_h);
let w = img_w * scale;
let h = img_h * scale;
(
scale,
scale,
rect.origin.x + (rect_w - w) / 2.0,
rect.origin.y + (rect_h - h) / 2.0,
)
}
ImageFit::None => (1.0, 1.0, rect.origin.x, rect.origin.y),
};
let transform = self
.current_state()
.transform
.post_translate(dx, dy)
.post_scale(scale_x, scale_y);
let clip = self.current_clip().cloned();
let surface = self.current_surface_mut();
let mut paint = PixmapPaint::default();
paint.quality = FilterQuality::Bilinear;
surface.draw_pixmap(
0,
0,
image.as_ref().as_ref(),
&paint,
transform,
clip.as_ref(),
);
Ok(())
}
fn draw_path(
&mut self,
path: &str,
fill: Option<&Fill>,
stroke: Option<&Stroke>,
bounds: fission_render::LayoutRect,
) -> Result<()> {
let bez = match BezPath::from_svg(path) {
Ok(path) => path,
Err(_) => return Ok(()),
};
let path = match bez_to_tiny_path(&bez) {
Some(path) => path,
None => return Ok(()),
};
let transform = self
.current_state()
.transform
.post_translate(bounds.origin.x, bounds.origin.y);
let clip = self.current_clip().cloned();
let surface = self.current_surface_mut();
if let Some(fill) = fill {
let paint = fill_paint(fill);
surface.fill_path(
&path,
&paint,
TinyFillRule::Winding,
transform,
clip.as_ref(),
);
}
if let Some(stroke) = stroke {
let paint = fill_paint(&stroke.fill);
let style = stroke_style(stroke);
surface.stroke_path(&path, &paint, &style, transform, clip.as_ref());
}
Ok(())
}
fn draw_svg(
&mut self,
content: &str,
fill: Option<&Fill>,
stroke: Option<&Stroke>,
bounds: fission_render::LayoutRect,
) -> Result<()> {
let entry = svg_cache_entry(content);
let (vb_x, vb_y, vb_w, vb_h) =
entry
.view_box
.unwrap_or((0.0, 0.0, bounds.width(), bounds.height()));
let rect_w = bounds.width();
let rect_h = bounds.height();
let (scale, dx, dy) = if vb_w > 0.0 && vb_h > 0.0 && rect_w > 0.0 && rect_h > 0.0 {
let scale = (rect_w / vb_w).min(rect_h / vb_h);
let scaled_w = vb_w * scale;
let scaled_h = vb_h * scale;
(
scale,
bounds.origin.x + (rect_w - scaled_w) / 2.0 - vb_x * scale,
bounds.origin.y + (rect_h - scaled_h) / 2.0 - vb_y * scale,
)
} else {
(1.0, bounds.origin.x, bounds.origin.y)
};
let transform = self
.current_state()
.transform
.post_translate(dx, dy)
.post_scale(scale, scale);
let clip = self.current_clip().cloned();
let surface = self.current_surface_mut();
for shape in &entry.shapes {
match shape {
SvgShape::Path(bez) => {
let Some(path) = bez_to_tiny_path(bez) else {
continue;
};
if let Some(fill) = fill {
let paint = fill_paint(fill);
surface.fill_path(
&path,
&paint,
TinyFillRule::Winding,
transform,
clip.as_ref(),
);
}
if let Some(stroke) = stroke {
let paint = fill_paint(&stroke.fill);
let style = stroke_style(stroke);
surface.stroke_path(&path, &paint, &style, transform, clip.as_ref());
}
}
SvgShape::Rect {
x,
y,
width,
height,
} => {
let Some(path) =
rect_path(fission_render::LayoutRect::new(*x, *y, *width, *height))
else {
continue;
};
if let Some(fill) = fill {
let paint = fill_paint(fill);
surface.fill_path(
&path,
&paint,
TinyFillRule::Winding,
transform,
clip.as_ref(),
);
}
if let Some(stroke) = stroke {
let paint = fill_paint(&stroke.fill);
let style = stroke_style(stroke);
surface.stroke_path(&path, &paint, &style, transform, clip.as_ref());
}
}
}
}
Ok(())
}
}