mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 22:42:46 +00:00
Add shared SVG subtree render handles
This commit is contained in:
1 file changed
+88
-10
@@ -1,13 +1,15 @@
|
||||
//! A format-neutral vector scene made from a bounded SVG tree.
|
||||
//!
|
||||
//! The scene preserves paths, groups, paints, transforms, opacity, strokes and image omissions
|
||||
//! The scene preserves paths, groups, paints, transforms, opacity, strokes and image sources
|
||||
//! without depending on an application's document model. Coordinates and stroke widths use the
|
||||
//! SVG tree's CSS-pixel coordinate system.
|
||||
|
||||
#[cfg(feature = "tree-render")]
|
||||
use super::draw::Refusal;
|
||||
use super::usvg;
|
||||
|
||||
/// A parsed SVG scene. Text is represented by its outlined paths.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Document {
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
@@ -16,12 +18,33 @@ pub struct Document {
|
||||
pub images: usize,
|
||||
/// True if nodes deeper than the requested depth were omitted.
|
||||
pub too_deep: bool,
|
||||
source: Option<Box<usvg::Tree>>,
|
||||
}
|
||||
|
||||
impl PartialEq for Document {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.width == other.width
|
||||
&& self.height == other.height
|
||||
&& self.nodes == other.nodes
|
||||
&& self.images == other.images
|
||||
&& self.too_deep == other.too_deep
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable node indices from the root of the source usvg tree.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct NodePath(Vec<usize>);
|
||||
|
||||
impl NodePath {
|
||||
pub fn indices(&self) -> &[usize] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A group or path in document order.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Node {
|
||||
Group { children: Vec<Node>, opacity: f32, transform: [f64; 6], bounds: [f64; 4], features: GroupFeatures },
|
||||
Group { children: Vec<Node>, opacity: f32, transform: [f64; 6], bounds: [f64; 4], features: GroupFeatures, source_path: NodePath },
|
||||
Path(Box<Path>),
|
||||
Image(Image),
|
||||
}
|
||||
@@ -45,6 +68,7 @@ pub struct Image {
|
||||
pub bounds: [f64; 4],
|
||||
pub encoded: Option<std::sync::Arc<Vec<u8>>>,
|
||||
pub svg: Option<Box<Document>>,
|
||||
pub source_path: NodePath,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -62,6 +86,7 @@ pub struct Path {
|
||||
pub segments: Vec<Segment>,
|
||||
pub fill: Option<Paint>,
|
||||
pub stroke: Option<Stroke>,
|
||||
pub source_path: NodePath,
|
||||
}
|
||||
|
||||
/// A path segment in CSS pixels after its absolute transform.
|
||||
@@ -111,6 +136,7 @@ pub struct Stroke {
|
||||
pub join: LineJoin,
|
||||
pub miter_limit: f64,
|
||||
pub dash_array: Option<Vec<f64>>,
|
||||
pub dash_offset: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -132,9 +158,53 @@ impl Document {
|
||||
pub fn from_tree(tree: &usvg::Tree, max_depth: usize) -> Self {
|
||||
let size = tree.size();
|
||||
let mut document = Self { width: f64::from(size.width()), height: f64::from(size.height()), ..Self::default() };
|
||||
document.nodes = convert_group(tree.root(), 0, max_depth, &mut document);
|
||||
document.nodes = convert_group(tree.root(), 0, max_depth, &mut document, &[]);
|
||||
document
|
||||
}
|
||||
|
||||
/// Build a scene that retains its source tree for fallback rendering.
|
||||
pub fn from_owned_tree(tree: usvg::Tree, max_depth: usize) -> Self {
|
||||
let size = tree.size();
|
||||
let mut document = Self { width: f64::from(size.width()), height: f64::from(size.height()), ..Self::default() };
|
||||
document.nodes = convert_group(tree.root(), 0, max_depth, &mut document, &[]);
|
||||
document.source = Some(Box::new(tree));
|
||||
document
|
||||
}
|
||||
|
||||
/// Draw a source subtree named by a [`NodePath`] without exposing usvg traversal to callers.
|
||||
#[cfg(feature = "tree-render")]
|
||||
pub fn draw_subtree(
|
||||
&self,
|
||||
path: &NodePath,
|
||||
transform: usvg::Transform,
|
||||
target: &mut super::draw::tiny_skia::Pixmap,
|
||||
held_bytes: u64,
|
||||
budget_bytes: u64,
|
||||
) -> Result<bool, Refusal> {
|
||||
let tree = self.source.as_deref().ok_or_else(|| Refusal {
|
||||
complex: true,
|
||||
memory: false,
|
||||
message: "the SVG scene does not retain its source tree".into(),
|
||||
})?;
|
||||
let mut group = tree.root();
|
||||
let mut node = None;
|
||||
for (i, index) in path.0.iter().enumerate() {
|
||||
node = Some(group.children().get(*index).ok_or_else(|| Refusal {
|
||||
complex: true,
|
||||
memory: false,
|
||||
message: "the SVG subtree path is no longer valid".into(),
|
||||
})?);
|
||||
if i + 1 < path.0.len() {
|
||||
group = match node {
|
||||
Some(usvg::Node::Group(g)) => g,
|
||||
Some(usvg::Node::Text(t)) => t.flattened(),
|
||||
_ => return Err(Refusal { complex: true, memory: false, message: "the SVG subtree path does not name a group".into() }),
|
||||
};
|
||||
}
|
||||
}
|
||||
let node = node.ok_or_else(|| Refusal { complex: true, memory: false, message: "the SVG subtree path is empty".into() })?;
|
||||
super::draw::render_node(node, transform, target, held_bytes, budget_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
fn transform(t: usvg::Transform) -> [f64; 6] {
|
||||
@@ -201,9 +271,11 @@ fn point(transform: usvg::Transform, p: usvg::tiny_skia_path::Point) -> [f64; 2]
|
||||
[f64::from(p.x), f64::from(p.y)]
|
||||
}
|
||||
|
||||
fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut Document) -> Vec<Node> {
|
||||
fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut Document, parent_path: &[usize]) -> Vec<Node> {
|
||||
let mut result = Vec::new();
|
||||
for node in group.children() {
|
||||
for (index, node) in group.children().iter().enumerate() {
|
||||
let mut source_path = parent_path.to_vec();
|
||||
source_path.push(index);
|
||||
match node {
|
||||
usvg::Node::Path(path) => {
|
||||
if !path.is_visible() || path.data().is_empty() {
|
||||
@@ -242,16 +314,17 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut
|
||||
},
|
||||
miter_limit: f64::from(s.miterlimit().get()),
|
||||
dash_array: s.dasharray().map(|d| d.iter().map(|v| f64::from(*v) * scale).collect()),
|
||||
dash_offset: f64::from(s.dashoffset()) * scale,
|
||||
}
|
||||
});
|
||||
result.push(Node::Path(Box::new(Path { segments, fill, stroke })));
|
||||
result.push(Node::Path(Box::new(Path { segments, fill, stroke, source_path: NodePath(source_path) })));
|
||||
}
|
||||
usvg::Node::Group(child) => {
|
||||
if depth >= max_depth {
|
||||
doc.too_deep = true;
|
||||
continue;
|
||||
}
|
||||
let children = convert_group(child, depth + 1, max_depth, doc);
|
||||
let children = convert_group(child, depth + 1, max_depth, doc, &source_path);
|
||||
if !children.is_empty() {
|
||||
result.push(Node::Group {
|
||||
children,
|
||||
@@ -265,6 +338,7 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut
|
||||
non_normal_blend: child.blend_mode() != usvg::BlendMode::Normal,
|
||||
isolation: child.should_isolate(),
|
||||
},
|
||||
source_path: NodePath(source_path),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -273,7 +347,7 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut
|
||||
doc.too_deep = true;
|
||||
continue;
|
||||
}
|
||||
let children = convert_group(text.flattened(), depth + 1, max_depth, doc);
|
||||
let children = convert_group(text.flattened(), depth + 1, max_depth, doc, &source_path);
|
||||
if !children.is_empty() {
|
||||
result.push(Node::Group {
|
||||
children,
|
||||
@@ -281,6 +355,7 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut
|
||||
transform: transform(text.abs_transform()),
|
||||
bounds: rect(text.abs_bounding_box()),
|
||||
features: GroupFeatures::default(),
|
||||
source_path: NodePath(source_path),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -297,7 +372,9 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut
|
||||
usvg::ImageKind::JPEG(data) | usvg::ImageKind::PNG(data) | usvg::ImageKind::GIF(data) | usvg::ImageKind::WEBP(data) => {
|
||||
(Some(data.clone()), None)
|
||||
}
|
||||
usvg::ImageKind::SVG(tree) => (None, Some(Box::new(Document::from_tree(tree, max_depth.saturating_sub(depth + 1))))),
|
||||
usvg::ImageKind::SVG(tree) => {
|
||||
(None, Some(Box::new(Document::from_owned_tree(tree.clone(), max_depth.saturating_sub(depth + 1)))))
|
||||
}
|
||||
};
|
||||
result.push(Node::Image(Image {
|
||||
kind,
|
||||
@@ -306,6 +383,7 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut
|
||||
bounds: rect(image.abs_bounding_box()),
|
||||
encoded,
|
||||
svg,
|
||||
source_path: NodePath(source_path),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user