2023-03-07 06:09:51 +01:00
|
|
|
use crate::core::svg::{Data, Handle};
|
2023-03-09 04:48:35 +01:00
|
|
|
use crate::core::{Color, Rectangle, Size};
|
2023-03-07 06:09:51 +01:00
|
|
|
|
2024-07-17 13:00:00 +02:00
|
|
|
use resvg::usvg;
|
2023-03-07 06:09:51 +01:00
|
|
|
use rustc_hash::{FxHashMap, FxHashSet};
|
2024-05-02 13:15:17 +02:00
|
|
|
use tiny_skia::Transform;
|
2023-03-07 06:09:51 +01:00
|
|
|
|
|
|
|
|
use std::cell::RefCell;
|
|
|
|
|
use std::collections::hash_map;
|
|
|
|
|
use std::fs;
|
2025-12-01 17:01:40 +01:00
|
|
|
use std::panic;
|
2024-09-14 05:43:00 +08:00
|
|
|
use std::sync::Arc;
|
2023-03-07 06:09:51 +01:00
|
|
|
|
2024-04-07 12:42:12 +02:00
|
|
|
#[derive(Debug)]
|
2023-03-07 06:09:51 +01:00
|
|
|
pub struct Pipeline {
|
|
|
|
|
cache: RefCell<Cache>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Pipeline {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
cache: RefCell::new(Cache::default()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn viewport_dimensions(&self, handle: &Handle) -> Size<u32> {
|
|
|
|
|
self.cache
|
|
|
|
|
.borrow_mut()
|
|
|
|
|
.viewport_dimensions(handle)
|
|
|
|
|
.unwrap_or(Size::new(0, 0))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn draw(
|
|
|
|
|
&mut self,
|
|
|
|
|
handle: &Handle,
|
2023-03-09 04:48:35 +01:00
|
|
|
color: Option<Color>,
|
2023-03-07 06:09:51 +01:00
|
|
|
bounds: Rectangle,
|
2024-05-03 13:25:58 +02:00
|
|
|
opacity: f32,
|
2023-03-07 06:09:51 +01:00
|
|
|
pixels: &mut tiny_skia::PixmapMut<'_>,
|
2024-05-02 13:15:17 +02:00
|
|
|
transform: Transform,
|
2023-04-26 16:46:27 +02:00
|
|
|
clip_mask: Option<&tiny_skia::Mask>,
|
2023-03-07 06:09:51 +01:00
|
|
|
) {
|
2023-03-09 04:48:35 +01:00
|
|
|
if let Some(image) = self.cache.borrow_mut().draw(
|
|
|
|
|
handle,
|
|
|
|
|
color,
|
2025-05-21 20:49:21 +02:00
|
|
|
Size::new(
|
|
|
|
|
(bounds.width * transform.sx) as u32,
|
|
|
|
|
(bounds.height * transform.sy) as u32,
|
|
|
|
|
),
|
2023-03-09 04:48:35 +01:00
|
|
|
) {
|
2023-03-07 06:09:51 +01:00
|
|
|
pixels.draw_pixmap(
|
2025-05-21 20:49:21 +02:00
|
|
|
(bounds.x * transform.sx) as i32,
|
|
|
|
|
(bounds.y * transform.sy) as i32,
|
2023-03-07 06:09:51 +01:00
|
|
|
image,
|
2024-05-03 13:25:58 +02:00
|
|
|
&tiny_skia::PixmapPaint {
|
|
|
|
|
opacity,
|
|
|
|
|
..tiny_skia::PixmapPaint::default()
|
|
|
|
|
},
|
2025-05-21 20:49:21 +02:00
|
|
|
Transform::default(),
|
2023-03-07 06:09:51 +01:00
|
|
|
clip_mask,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn trim_cache(&mut self) {
|
|
|
|
|
self.cache.borrow_mut().trim();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
struct Cache {
|
|
|
|
|
trees: FxHashMap<u64, Option<resvg::usvg::Tree>>,
|
|
|
|
|
tree_hits: FxHashSet<u64>,
|
2023-03-09 04:48:35 +01:00
|
|
|
rasters: FxHashMap<RasterKey, tiny_skia::Pixmap>,
|
|
|
|
|
raster_hits: FxHashSet<RasterKey>,
|
2024-09-14 05:43:00 +08:00
|
|
|
fontdb: Option<Arc<usvg::fontdb::Database>>,
|
2023-03-09 04:48:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
|
|
|
struct RasterKey {
|
|
|
|
|
id: u64,
|
|
|
|
|
color: Option<[u8; 4]>,
|
|
|
|
|
size: Size<u32>,
|
2023-03-07 06:09:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Cache {
|
|
|
|
|
fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> {
|
|
|
|
|
let id = handle.id();
|
|
|
|
|
|
2024-09-14 05:43:00 +08:00
|
|
|
// TODO: Reuse `cosmic-text` font database
|
|
|
|
|
if self.fontdb.is_none() {
|
|
|
|
|
let mut fontdb = usvg::fontdb::Database::new();
|
|
|
|
|
fontdb.load_system_fonts();
|
|
|
|
|
|
|
|
|
|
self.fontdb = Some(Arc::new(fontdb));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let options = usvg::Options {
|
|
|
|
|
fontdb: self
|
|
|
|
|
.fontdb
|
|
|
|
|
.as_ref()
|
|
|
|
|
.expect("fontdb must be initialized")
|
|
|
|
|
.clone(),
|
|
|
|
|
..usvg::Options::default()
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-07 06:09:51 +01:00
|
|
|
if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) {
|
2024-07-17 13:00:00 +02:00
|
|
|
let svg = match handle.data() {
|
2023-03-07 06:09:51 +01:00
|
|
|
Data::Path(path) => {
|
|
|
|
|
fs::read_to_string(path).ok().and_then(|contents| {
|
2024-09-14 05:43:00 +08:00
|
|
|
usvg::Tree::from_str(&contents, &options).ok()
|
2023-03-07 06:09:51 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
Data::Bytes(bytes) => {
|
2024-09-14 05:43:00 +08:00
|
|
|
usvg::Tree::from_data(bytes, &options).ok()
|
2023-03-07 06:09:51 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-09-09 20:58:45 +02:00
|
|
|
let _ = entry.insert(svg);
|
2023-03-07 06:09:51 +01:00
|
|
|
}
|
|
|
|
|
|
2023-09-09 20:58:45 +02:00
|
|
|
let _ = self.tree_hits.insert(id);
|
2023-03-07 06:09:51 +01:00
|
|
|
self.trees.get(&id).unwrap().as_ref()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn viewport_dimensions(&mut self, handle: &Handle) -> Option<Size<u32>> {
|
|
|
|
|
let tree = self.load(handle)?;
|
2024-07-17 13:00:00 +02:00
|
|
|
let size = tree.size();
|
2023-03-07 06:09:51 +01:00
|
|
|
|
2024-07-17 13:00:00 +02:00
|
|
|
Some(Size::new(size.width() as u32, size.height() as u32))
|
2023-03-07 06:09:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn draw(
|
|
|
|
|
&mut self,
|
|
|
|
|
handle: &Handle,
|
2023-03-09 04:48:35 +01:00
|
|
|
color: Option<Color>,
|
2023-03-07 06:09:51 +01:00
|
|
|
size: Size<u32>,
|
|
|
|
|
) -> Option<tiny_skia::PixmapRef<'_>> {
|
|
|
|
|
if size.width == 0 || size.height == 0 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-09 04:48:35 +01:00
|
|
|
let key = RasterKey {
|
|
|
|
|
id: handle.id(),
|
|
|
|
|
color: color.map(Color::into_rgba8),
|
|
|
|
|
size,
|
|
|
|
|
};
|
2023-03-07 06:09:51 +01:00
|
|
|
|
2023-03-07 06:34:27 +01:00
|
|
|
#[allow(clippy::map_entry)]
|
2023-03-09 04:48:35 +01:00
|
|
|
if !self.rasters.contains_key(&key) {
|
2023-03-07 06:09:51 +01:00
|
|
|
let tree = self.load(handle)?;
|
|
|
|
|
|
|
|
|
|
let mut image = tiny_skia::Pixmap::new(size.width, size.height)?;
|
|
|
|
|
|
2024-07-17 13:00:00 +02:00
|
|
|
let tree_size = tree.size().to_int_size();
|
2023-07-12 09:07:20 +02:00
|
|
|
|
|
|
|
|
let target_size = if size.width > size.height {
|
|
|
|
|
tree_size.scale_to_width(size.width)
|
2023-06-10 13:18:42 -07:00
|
|
|
} else {
|
2023-07-12 09:07:20 +02:00
|
|
|
tree_size.scale_to_height(size.height)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let transform = if let Some(target_size) = target_size {
|
2023-06-10 13:18:42 -07:00
|
|
|
let tree_size = tree_size.to_size();
|
|
|
|
|
let target_size = target_size.to_size();
|
2023-07-12 09:07:20 +02:00
|
|
|
|
|
|
|
|
tiny_skia::Transform::from_scale(
|
2023-06-10 13:18:42 -07:00
|
|
|
target_size.width() / tree_size.width(),
|
|
|
|
|
target_size.height() / tree_size.height(),
|
2023-07-12 09:07:20 +02:00
|
|
|
)
|
2023-06-10 13:18:42 -07:00
|
|
|
} else {
|
2023-07-12 09:07:20 +02:00
|
|
|
tiny_skia::Transform::default()
|
|
|
|
|
};
|
2023-06-10 13:18:42 -07:00
|
|
|
|
2025-11-23 12:39:22 +01:00
|
|
|
// SVG rendering can panic on malformed or complex vectors.
|
|
|
|
|
// We catch panics to prevent crashes and continue gracefully.
|
2025-12-01 17:01:40 +01:00
|
|
|
let render = panic::catch_unwind(panic::AssertUnwindSafe(|| {
|
|
|
|
|
resvg::render(tree, transform, &mut image.as_mut());
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
if let Err(error) = render {
|
|
|
|
|
log::warn!("SVG rendering for {handle:?} panicked: {error:?}");
|
2025-11-23 12:39:22 +01:00
|
|
|
}
|
2023-03-07 06:09:51 +01:00
|
|
|
|
2023-05-09 14:57:50 -04:00
|
|
|
if let Some([r, g, b, _]) = key.color {
|
2023-03-09 04:48:35 +01:00
|
|
|
// Apply color filter
|
|
|
|
|
for pixel in
|
|
|
|
|
bytemuck::cast_slice_mut::<u8, u32>(image.data_mut())
|
|
|
|
|
{
|
2023-07-12 09:03:24 +02:00
|
|
|
*pixel = bytemuck::cast(
|
2023-06-10 13:18:42 -07:00
|
|
|
tiny_skia::ColorU8::from_rgba(
|
|
|
|
|
b,
|
|
|
|
|
g,
|
|
|
|
|
r,
|
|
|
|
|
(*pixel >> 24) as u8,
|
|
|
|
|
)
|
|
|
|
|
.premultiply(),
|
|
|
|
|
);
|
2023-03-09 04:48:35 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Swap R and B channels for `softbuffer` presentation
|
|
|
|
|
for pixel in
|
|
|
|
|
bytemuck::cast_slice_mut::<u8, u32>(image.data_mut())
|
|
|
|
|
{
|
2023-09-20 05:19:24 +02:00
|
|
|
*pixel = *pixel & 0xFF00_FF00
|
|
|
|
|
| ((0x0000_00FF & *pixel) << 16)
|
|
|
|
|
| ((0x00FF_0000 & *pixel) >> 16);
|
2023-03-09 04:48:35 +01:00
|
|
|
}
|
2023-03-07 06:09:51 +01:00
|
|
|
}
|
|
|
|
|
|
2023-09-09 20:58:45 +02:00
|
|
|
let _ = self.rasters.insert(key, image);
|
2023-03-07 06:09:51 +01:00
|
|
|
}
|
|
|
|
|
|
2023-09-09 20:58:45 +02:00
|
|
|
let _ = self.raster_hits.insert(key);
|
2023-03-09 04:48:35 +01:00
|
|
|
self.rasters.get(&key).map(tiny_skia::Pixmap::as_ref)
|
2023-03-07 06:09:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn trim(&mut self) {
|
|
|
|
|
self.trees.retain(|key, _| self.tree_hits.contains(key));
|
|
|
|
|
self.rasters.retain(|key, _| self.raster_hits.contains(key));
|
|
|
|
|
|
|
|
|
|
self.tree_hits.clear();
|
|
|
|
|
self.raster_hits.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-07 12:42:12 +02:00
|
|
|
|
|
|
|
|
impl std::fmt::Debug for Cache {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
f.debug_struct("Cache")
|
|
|
|
|
.field("tree_hits", &self.tree_hits)
|
|
|
|
|
.field("rasters", &self.rasters)
|
|
|
|
|
.field("raster_hits", &self.raster_hits)
|
|
|
|
|
.finish_non_exhaustive()
|
|
|
|
|
}
|
|
|
|
|
}
|