blob: 1c230eb9a927ae586fc342e826a7111d83be73df [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use std::path::Path;
use std::sync::OnceLock;
use usvg::tiny_skia_path::PathSegment;
use usvg::{Group, Node};
use vello_common::fearless_simd::Level;
use vello_common::flatten::{FlattenCtx, Line};
use vello_common::kurbo::{Affine, BezPath, Stroke};
use vello_common::peniko::Fill;
use vello_common::strip::Strip;
use vello_common::tile::Tiles;
use vello_common::{flatten, strip};
static DATA: OnceLock<Vec<DataItem>> = OnceLock::new();
pub fn get_data_items() -> &'static [DataItem] {
DATA.get_or_init(|| {
let data_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("data");
let mut data = vec![];
// Always use ghostscript tiger.
data.push(DataItem::from_path(
&Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../examples/assets/Ghostscript_Tiger.svg"),
));
for entry in std::fs::read_dir(&data_dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("svg") {
data.push(DataItem::from_path(&path));
}
}
data
})
}
#[derive(Clone, Debug)]
pub struct DataItem {
pub name: String,
pub fills: Vec<FilledPath>,
pub strokes: Vec<StrokedPath>,
pub width: u16,
pub height: u16,
}
impl DataItem {
fn from_path(path: &Path) -> Self {
let file_name = { path.file_stem().unwrap().to_string_lossy().to_string() };
let data = std::fs::read(path).unwrap();
let tree = usvg::Tree::from_data(&data, &usvg::Options::default()).unwrap();
let mut ctx = ConversionContext::new();
convert(&mut ctx, tree.root());
Self {
name: file_name,
fills: ctx.fills,
strokes: ctx.strokes,
#[expect(
clippy::cast_possible_truncation,
reason = "It's okay to ignore for benchmarking."
)]
width: tree.size().width() as u16,
#[expect(
clippy::cast_possible_truncation,
reason = "It's okay to ignore for benchmarking."
)]
height: tree.size().height() as u16,
}
}
/// Get the raw flattened lines of both fills and strokes.
pub fn lines(&self) -> Vec<Line> {
let mut line_buf = vec![];
let mut temp_buf = vec![];
for path in &self.fills {
flatten::fill(
Level::new(),
&path.path,
path.transform,
&mut temp_buf,
&mut FlattenCtx::default(),
);
line_buf.extend(&temp_buf);
}
for path in &self.strokes {
let stroke = Stroke {
width: path.stroke_width as f64,
..Default::default()
};
flatten::stroke(
Level::new(),
&path.path,
&stroke,
path.transform,
&mut temp_buf,
&mut FlattenCtx::default(),
);
line_buf.extend(&temp_buf);
}
line_buf
}
/// Get the expanded strokes.
pub fn expanded_strokes(&self) -> Vec<BezPath> {
let mut paths = vec![];
for path in &self.strokes {
let stroke = Stroke {
width: path.stroke_width as f64,
..Default::default()
};
paths.push(flatten::expand_stroke(path.path.iter(), &stroke, 0.25));
}
paths
}
/// Get the unsorted tiles.
pub fn unsorted_tiles(&self) -> Tiles {
let mut tiles = Tiles::new();
let lines = self.lines();
tiles.make_tiles(&lines, self.width, self.height);
tiles
}
/// Get the sorted tiles.
pub fn sorted_tiles(&self) -> Tiles {
let mut tiles = self.unsorted_tiles();
tiles.sort_tiles();
tiles
}
/// Get the alpha buffer and rendered strips.
pub fn strips(&self) -> (Vec<u8>, Vec<Strip>) {
let mut strip_buf = vec![];
let mut alpha_buf = vec![];
let lines = self.lines();
let tiles = self.sorted_tiles();
strip::render(
Level::fallback(),
&tiles,
&mut strip_buf,
&mut alpha_buf,
Fill::NonZero,
&lines,
);
(alpha_buf, strip_buf)
}
}
fn convert(ctx: &mut ConversionContext, g: &Group) {
ctx.push(convert_transform(&g.transform()));
for child in g.children() {
match child {
Node::Group(group) => {
convert(ctx, group);
}
Node::Path(p) => {
let converted = convert_path_data(p);
if p.fill().is_some() {
ctx.add_filled_path(converted.clone());
}
if let Some(stroke) = p.stroke() {
ctx.add_stroked_path(converted, stroke.width().get());
}
}
Node::Image(_) | Node::Text(_) => {}
}
}
ctx.pop();
}
#[derive(Debug, Clone)]
pub struct FilledPath {
pub path: BezPath,
pub transform: Affine,
}
#[derive(Debug, Clone)]
pub struct StrokedPath {
pub path: BezPath,
pub transform: Affine,
pub stroke_width: f32,
}
#[derive(Debug)]
struct ConversionContext {
stack: Vec<Affine>,
fills: Vec<FilledPath>,
strokes: Vec<StrokedPath>,
}
impl ConversionContext {
fn new() -> Self {
Self {
stack: vec![],
fills: vec![],
strokes: vec![],
}
}
fn push(&mut self, transform: Affine) {
let new = *self.stack.last().unwrap_or(&Affine::IDENTITY) * transform;
self.stack.push(new);
}
fn add_filled_path(&mut self, path: BezPath) {
self.fills.push(FilledPath {
path,
transform: self.get(),
});
}
fn add_stroked_path(&mut self, path: BezPath, stroke_width: f32) {
self.strokes.push(StrokedPath {
path,
transform: self.get(),
stroke_width,
});
}
fn get(&self) -> Affine {
*self.stack.last().unwrap_or(&Affine::IDENTITY)
}
fn pop(&mut self) {
self.stack.pop();
}
}
fn convert_transform(transform: &usvg::Transform) -> Affine {
Affine::new([
transform.sx as f64,
transform.ky as f64,
transform.kx as f64,
transform.sy as f64,
transform.tx as f64,
transform.ty as f64,
])
}
fn convert_path_data(path: &usvg::Path) -> BezPath {
let mut bez_path = BezPath::new();
for e in path.data().segments() {
match e {
PathSegment::MoveTo(p) => {
bez_path.move_to((p.x, p.y));
}
PathSegment::LineTo(p) => {
bez_path.line_to((p.x, p.y));
}
PathSegment::QuadTo(p1, p2) => {
bez_path.quad_to((p1.x, p1.y), (p2.x, p2.y));
}
PathSegment::CubicTo(p1, p2, p3) => {
bez_path.curve_to((p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y));
}
PathSegment::Close => {
bez_path.close_path();
}
}
}
bez_path
}