Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ lilt.workspace = true
log.workspace = true
num-traits.workspace = true
raw-window-handle.workspace = true
resvg.workspace = true
rustc-hash.workspace = true
smol_str.workspace = true
thiserror.workspace = true
Expand Down
119 changes: 94 additions & 25 deletions core/src/svg.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,55 @@
//! Load and draw vector graphics.
use crate::{Color, Radians, Rectangle, Size};
use crate::{Bytes, Color, Radians, Rectangle, Size};

use resvg::usvg;
use rustc_hash::FxHasher;
use std::borrow::Cow;
use std::hash::{Hash, Hasher as _};
use std::fmt::{self, Debug};
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::ptr;
use std::sync::Arc;

/// The unique identifier of some [`Handle`] data.
#[derive(Clone)]
pub enum Id {
/// Hash value of [`Data`]
Hash(u64),
/// Address of allocated [`usvg::Tree`]
Addr(Arc<usvg::Tree>),
}

impl Debug for Id {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
&Self::Hash(hash) => write!(f, "{}", hash),
Self::Addr(addr) => write!(f, "{:?}", addr.as_ref() as *const usvg::Tree),
}
}
}

impl PartialEq for Id {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(&Self::Hash(x), &Self::Hash(y)) => x == y,
(Self::Addr(x), Self::Addr(y)) => ptr::eq(x.as_ref(), y.as_ref()),
_ => false,
}
}
}

impl Eq for Id {}

impl Hash for Id {
fn hash<H: Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
&Id::Hash(x) => state.write_u64(x),
Id::Addr(tree) => (tree.as_ref() as *const usvg::Tree).hash(state),
}
}
}

/// A raster image that can be drawn.
#[derive(Debug, Clone, PartialEq)]
pub struct Svg<H = Handle> {
Expand Down Expand Up @@ -68,10 +111,18 @@ impl From<&Handle> for Svg {
}

/// A handle of Svg data.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Handle {
id: u64,
data: Arc<Data>,
#[derive(Debug, Clone)]
pub enum Handle {
/// Unloaded svg [`Data`]
Unloaded {
/// Hash value of [`Handle::Unloaded::data`]
hash: u64,
/// Data storage of a [`Handle`]
data: Data,
},

/// Parsed [`usvg::Tree`]
Loaded(Arc<usvg::Tree>),
}

impl Handle {
Expand All @@ -86,28 +137,31 @@ impl Handle {
///
/// This is useful if you already have your SVG data in-memory, maybe
/// because you downloaded or generated it procedurally.
pub fn from_memory(bytes: impl Into<Cow<'static, [u8]>>) -> Handle {
pub fn from_memory(bytes: impl Into<Bytes>) -> Handle {
Self::from_data(Data::Bytes(bytes.into()))
}

fn from_data(data: Data) -> Handle {
let mut hasher = FxHasher::default();
data.hash(&mut hasher);

Handle {
id: hasher.finish(),
data: Arc::new(data),
Handle::Unloaded {
hash: hasher.finish(),
data,
}
}

/// Returns the unique identifier of the [`Handle`].
pub fn id(&self) -> u64 {
self.id
/// Creates an SVG [`Handle`] from a parsed `usvg::Tree`
pub fn from_tree(tree: Arc<usvg::Tree>) -> Handle {
Self::Loaded(tree)
}

/// Returns a reference to the SVG [`Data`].
pub fn data(&self) -> &Data {
&self.data
/// Returns the unique identifier of the [`Handle`].
pub fn id(&self) -> Id {
match self {
&Handle::Unloaded { hash, .. } => Id::Hash(hash),
Handle::Loaded(tree) => Id::Addr(tree.clone()),
}
}
}

Expand All @@ -120,9 +174,12 @@ where
}
}

impl Hash for Handle {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
impl PartialEq for Handle {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(&Handle::Unloaded { hash: x, .. }, &Handle::Unloaded { hash: y, .. }) => x == y,
_ => false,
}
}
}

Expand All @@ -135,14 +192,26 @@ pub enum Data {
/// In-memory data
///
/// Can contain an SVG string or a gzip compressed data.
Bytes(Cow<'static, [u8]>),
Bytes(Bytes),
}

impl Data {
/// Try to load and parse `Data` to `usvg::Tree`
pub fn load(&self, options: &usvg::Options<'_>) -> Option<usvg::Tree> {
match self {
Self::Path(path) => fs::read_to_string(path)
.ok()
.and_then(|text| usvg::Tree::from_str(&text, options).ok()),
Data::Bytes(bytes) => usvg::Tree::from_data(bytes, options).ok(),
}
}
}

impl std::fmt::Debug for Data {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Debug for Data {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Data::Path(path) => write!(f, "Path({path:?})"),
Data::Bytes(_) => write!(f, "Bytes(...)"),
Self::Path(path) => f.debug_tuple("Path").field(path).finish(),
Self::Bytes(_) => f.write_str("Bytes(...)"),
}
}
}
Expand Down
59 changes: 25 additions & 34 deletions tiny_skia/src/vector.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
use crate::core::svg::{Data, Handle};
use crate::core::svg::{Handle, Id};
use crate::core::{Color, Rectangle, Size};

use resvg::usvg;
use rustc_hash::{FxHashMap, FxHashSet};
use tiny_skia::Transform;

use std::cell::RefCell;
use std::collections::hash_map;
use std::fs;
use std::panic;
#[cfg(feature = "svg-text")]
use std::sync::Arc;

#[derive(Debug)]
Expand Down Expand Up @@ -70,25 +67,23 @@ impl Pipeline {

#[derive(Default)]
struct Cache {
trees: FxHashMap<u64, Option<resvg::usvg::Tree>>,
trees: FxHashMap<u64, Option<Arc<usvg::Tree>>>,
tree_hits: FxHashSet<u64>,
rasters: FxHashMap<RasterKey, tiny_skia::Pixmap>,
raster_hits: FxHashSet<RasterKey>,
#[cfg(feature = "svg-text")]
fontdb: Option<Arc<usvg::fontdb::Database>>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct RasterKey {
id: u64,
id: Id,
color: Option<[u8; 4]>,
size: Size<u32>,
}

impl Cache {
fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> {
let id = handle.id();

fn load(&mut self, handle: &Handle) -> Option<Arc<usvg::Tree>> {
// TODO: Reuse `cosmic-text` font database
#[cfg(feature = "svg-text")]
if self.fontdb.is_none() {
Expand All @@ -98,29 +93,25 @@ impl Cache {
self.fontdb = Some(Arc::new(fontdb));
}

let options = usvg::Options {
#[cfg(feature = "svg-text")]
fontdb: self
.fontdb
.as_ref()
.expect("fontdb must be initialized")
match handle {
&Handle::Unloaded { hash, ref data } => self
.trees
.entry(hash)
.or_insert_with(|| {
data.load(&usvg::Options {
#[cfg(feature = "svg-text")]
fontdb: self
.fontdb
.as_ref()
.expect("fontdb must be initialized")
.clone(),
..usvg::Options::default()
})
.map(Arc::new)
})
.clone(),
..usvg::Options::default()
};

if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) {
let svg = match handle.data() {
Data::Path(path) => fs::read_to_string(path)
.ok()
.and_then(|contents| usvg::Tree::from_str(&contents, &options).ok()),
Data::Bytes(bytes) => usvg::Tree::from_data(bytes, &options).ok(),
};

let _ = entry.insert(svg);
Handle::Loaded(tree) => Some(tree.clone()),
}

let _ = self.tree_hits.insert(id);
self.trees.get(&id).unwrap().as_ref()
}

fn viewport_dimensions(&mut self, handle: &Handle) -> Option<Size<u32>> {
Expand Down Expand Up @@ -175,7 +166,7 @@ impl Cache {
// SVG rendering can panic on malformed or complex vectors.
// We catch panics to prevent crashes and continue gracefully.
let render = panic::catch_unwind(panic::AssertUnwindSafe(|| {
resvg::render(tree, transform, &mut image.as_mut());
resvg::render(&tree, transform, &mut image.as_mut());
}));

if let Err(error) = render {
Expand All @@ -198,10 +189,10 @@ impl Cache {
}
}

let _ = self.rasters.insert(key, image);
let _ = self.rasters.insert(key.clone(), image);
}

let _ = self.raster_hits.insert(key);
let _ = self.raster_hits.insert(key.clone());
self.rasters.get(&key).map(tiny_skia::Pixmap::as_ref)
}

Expand Down
Loading
Loading