blob: ebc47c97b5271411585a7e617459dd4bcbd89efc [file] [edit]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! A very basic SVG renderer built on top of `usvg` and `vello_cpu`.
#![allow(
clippy::cast_possible_truncation,
reason = "this is only a debug tool, so we can ignore them"
)]
use clap::Parser;
use image::codecs::png::PngEncoder;
use image::{ExtendedColorType, ImageEncoder};
use std::io::Cursor;
use std::path;
use std::path::Path;
use std::time::{Duration, Instant};
use usvg::tiny_skia_path::PathSegment;
use usvg::{Node, Paint, PaintOrder};
use vello_cpu::color::AlphaColor;
use vello_cpu::kurbo::{Affine, BezPath, Stroke};
use vello_cpu::peniko::{Fill, ImageAlphaType};
use vello_cpu::{Level, Pixmap, RenderContext, RenderSettings, Resources};
fn main() {
let args = Args::parse();
let scale = args.scale;
let path = Path::new(&args.path);
let svg = std::fs::read_to_string(path).expect("error reading SVG file");
let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap();
let width = (tree.size().width() * scale).ceil() as u16;
let height = (tree.size().height() * scale).ceil() as u16;
let mut num_iters = 0;
let settings = RenderSettings {
level: Level::new(),
num_threads: args.num_threads as u16,
};
let mut ctx = RenderContext::new_with(width, height, settings);
let mut resources = Resources::new();
let mut pixmap = Pixmap::new(width, height);
let mut runtime = Duration::default();
loop {
ctx.reset();
let mut sctx = SVGContext::new_with_scale(scale as f64);
let start = Instant::now();
render_tree(&mut ctx, &mut sctx, &tree);
ctx.flush();
ctx.render(&mut pixmap, &mut resources);
runtime += start.elapsed();
num_iters += 1;
if runtime.as_millis() > args.runtime as u128 {
break;
}
}
let avg_runtime = (runtime.as_millis() as f32) / (num_iters as f32);
eprintln!("average runtime {avg_runtime}ms");
write_pixmap(&mut pixmap);
}
fn write_pixmap(pixmap: &mut Pixmap) {
let data = pixmap.clone().take_rgba8(ImageAlphaType::Alpha);
let mut png_data = Vec::new();
let cursor = Cursor::new(&mut png_data);
let encoder = PngEncoder::new(cursor);
encoder
.write_image(
&data,
pixmap.width() as u32,
pixmap.height() as u32,
ExtendedColorType::Rgba8,
)
.expect("failed to encode image");
let path = path::absolute("svg.png").unwrap();
std::fs::write(&path, png_data).unwrap();
eprintln!("saved rendered SVG to '{}'", path.display());
}
#[derive(Parser, Debug)]
struct Args {
/// How much to scale the output image.
#[arg(long, default_value_t = 1.0)]
pub scale: f32,
/// The path to the SVG file.
#[arg(long)]
pub path: String,
/// For simple benchmarking, our current recommended value is 2000 (i.e. 2 seconds).
#[arg(long, default_value_t = 0)]
pub runtime: u32,
/// The number of additional threads that should be used for rendering.
#[arg(long, default_value_t = 0)]
pub num_threads: u32,
}
struct SVGContext {
transforms: Vec<Affine>,
}
impl Default for SVGContext {
fn default() -> Self {
Self::new()
}
}
impl SVGContext {
fn new() -> Self {
Self {
transforms: vec![Affine::IDENTITY],
}
}
fn new_with_scale(scale: f64) -> Self {
Self {
transforms: vec![Affine::scale(scale)],
}
}
fn push_transform(&mut self, affine: &Affine) {
let new = *self.transforms.last().unwrap() * *affine;
self.transforms.push(new);
}
fn pop_transform(&mut self) {
self.transforms.pop();
}
fn get_transform(&self) -> Affine {
*self.transforms.last().unwrap()
}
}
fn render_tree(ctx: &mut RenderContext, sctx: &mut SVGContext, tree: &usvg::Tree) {
render_group(ctx, sctx, tree.root());
}
fn render_group(ctx: &mut RenderContext, sctx: &mut SVGContext, group: &usvg::Group) {
let clip_path = {
group.clip_path().map(|p| {
let mut path = BezPath::new();
extract_clip_path(p.root(), &mut path);
convert_transform(&p.transform()) * path
})
};
ctx.push_layer(clip_path.as_ref(), None, None, None, None);
sctx.push_transform(&convert_transform(&group.transform()));
for child in group.children() {
match child {
Node::Group(g) => {
render_group(ctx, sctx, g);
}
Node::Path(p) => {
render_path(ctx, sctx, p);
}
Node::Image(_) => {}
Node::Text(_) => {}
}
}
ctx.pop_layer();
sctx.pop_transform();
}
// This is only very crude, as in theory we also have to consider nested clip paths, fill rule,
// etc. For now, we just assume a flat hierarchy of clip paths with nonzero filling.
fn extract_clip_path(group: &usvg::Group, path: &mut BezPath) {
for child in group.children() {
match child {
Node::Group(g) => {
extract_clip_path(g, path);
}
Node::Path(p) => {
path.extend(convert_path_data(p));
}
Node::Image(_) => {}
Node::Text(_) => {}
}
}
}
fn render_path(ctx: &mut RenderContext, sctx: &mut SVGContext, path: &usvg::Path) {
if !path.is_visible() {
return;
}
ctx.set_transform(sctx.get_transform());
let fill = |rctx: &mut RenderContext, p: &usvg::Path| {
if let Some(fill) = p.fill() {
let color = match fill.paint() {
Paint::Color(c) => {
AlphaColor::from_rgba8(c.red, c.green, c.blue, fill.opacity().to_u8())
}
_ => return,
};
rctx.set_fill_rule(convert_fill_rule(fill.rule()));
rctx.set_paint(color);
rctx.fill_path(&convert_path_data(p));
}
};
let stroke = |rctx: &mut RenderContext, p: &usvg::Path| {
if let Some(stroke) = p.stroke() {
let color = match stroke.paint() {
Paint::Color(c) => {
AlphaColor::from_rgba8(c.red, c.green, c.blue, stroke.opacity().to_u8())
}
_ => return,
};
let stroke = Stroke::new(stroke.width().get() as f64);
rctx.set_stroke(stroke);
rctx.set_paint(color);
rctx.stroke_path(&convert_path_data(p));
}
};
if path.paint_order() == PaintOrder::FillAndStroke {
fill(ctx, path);
stroke(ctx, path);
} else {
stroke(ctx, path);
fill(ctx, path);
}
}
fn convert_fill_rule(fill_rule: usvg::FillRule) -> Fill {
match fill_rule {
usvg::FillRule::NonZero => Fill::NonZero,
usvg::FillRule::EvenOdd => Fill::EvenOdd,
}
}
fn convert_transform(transform: &usvg::Transform) -> Affine {
Affine::new([
transform.sx as f64,
// TODO: It's possible that it should be kx first and then ky
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
}