use std::time::{Duration, Instant};
use resvg::{tiny_skia, usvg};
use saudade::{
App, Color, FontSet, Painter, Rect, SvgImage, Theme, Widget, WindowConfig, include_svg,
};
struct Icon {
name: &'static str,
baked: SvgImage,
source: &'static str,
}
const ICONS: &[Icon] = &[
Icon {
name: "sound",
baked: include_svg!("examples/assets/icons/sound.svg"),
source: include_str!("assets/icons/sound.svg"),
},
Icon {
name: "colors",
baked: include_svg!("examples/assets/icons/colors.svg"),
source: include_str!("assets/icons/colors.svg"),
},
Icon {
name: "network",
baked: include_svg!("examples/assets/icons/network.svg"),
source: include_str!("assets/icons/network.svg"),
},
Icon {
name: "printers",
baked: include_svg!("examples/assets/icons/printers.svg"),
source: include_str!("assets/icons/printers.svg"),
},
Icon {
name: "audio",
baked: include_svg!("examples/assets/icons/audio.svg"),
source: include_str!("assets/icons/audio.svg"),
},
Icon {
name: "display",
baked: include_svg!("examples/assets/icons/display.svg"),
source: include_str!("assets/icons/display.svg"),
},
];
const W: i32 = 660;
const H: i32 = 430;
fn main() {
benchmark();
App::new(
WindowConfig::new("include_svg! vs include_str!", W, H),
Demo,
)
.with_theme(Theme::windows_31())
.run();
}
fn rasterize_text(svg: &str, w: u32, h: u32) -> Vec<u32> {
let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).expect("valid demo SVG");
rasterize_tree(&tree, w, h)
}
fn rasterize_tree(tree: &usvg::Tree, w: u32, h: u32) -> Vec<u32> {
let mut pixmap = tiny_skia::Pixmap::new(w, h).expect("non-zero size");
let size = tree.size();
let scale = (w as f32 / size.width()).min(h as f32 / size.height());
let tx = (w as f32 - size.width() * scale) * 0.5;
let ty = (h as f32 - size.height() * scale) * 0.5;
let transform = tiny_skia::Transform::from_scale(scale, scale).post_translate(tx, ty);
resvg::render(tree, transform, &mut pixmap.as_mut());
let mut out = vec![Color::WHITE.0; (w * h) as usize];
for (px, chunk) in out.iter_mut().zip(pixmap.data().chunks_exact(4)) {
let (r, g, b, a) = (chunk[0], chunk[1], chunk[2], chunk[3]);
if a == 0 {
continue;
}
let inv = 255 - a as u32;
let comp = |c: u8| (c as u32 + inv).min(255);
*px = 0xFF00_0000 | (comp(r) << 16) | (comp(g) << 8) | comp(b);
}
out
}
fn draw_rasterized(painter: &mut Painter, svg: &str, rect: Rect) {
painter.physical(rect, |p, phys| {
if phys.w <= 0 || phys.h <= 0 {
return;
}
let raster = rasterize_text(svg, phys.w as u32, phys.h as u32);
for py in 0..phys.h {
let row = (py * phys.w) as usize;
for px in 0..phys.w {
p.pixel(phys.x + px, phys.y + py, Color(raster[row + px as usize]));
}
}
});
}
struct Demo;
impl Widget for Demo {
fn bounds(&self) -> Rect {
Rect::new(0, 0, W, H)
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
painter.fill(Color::WHITE);
painter.text(
16,
12,
"include_svg! vs include_str! + resvg",
14.0,
theme.text,
);
painter.text(
16,
32,
"Same six icons, two pipelines. They should look identical.",
11.0,
theme.disabled_text,
);
let cell = 56;
let gap = 44;
let x0 = 150;
let size = 40;
let pad = (cell - size) / 2;
let baked_y = 64;
let rast_y = baked_y + cell + 20;
painter.text(
16,
baked_y + cell / 2 - 14,
"include_svg!",
11.0,
theme.text,
);
painter.text(
16,
baked_y + cell / 2,
"(baked polygons)",
10.0,
theme.disabled_text,
);
painter.text(16, rast_y + cell / 2 - 14, "include_str!", 11.0, theme.text);
painter.text(
16,
rast_y + cell / 2,
"+ resvg (runtime)",
10.0,
theme.disabled_text,
);
for (i, icon) in ICONS.iter().enumerate() {
let x = x0 + i as i32 * (cell + gap);
painter.stroke_rect(Rect::new(x, baked_y, cell, cell), theme.shadow);
painter.stroke_rect(Rect::new(x, rast_y, cell, cell), theme.shadow);
icon.baked
.draw(painter, Rect::new(x + pad, baked_y + pad, size, size));
draw_rasterized(
painter,
icon.source,
Rect::new(x + pad, rast_y + pad, size, size),
);
painter.text_centered(
Rect::new(x, rast_y + cell + 2, cell, 14),
icon.name,
10.0,
theme.disabled_text,
);
}
let ramp = &ICONS[1]; let ramp_y = rast_y + cell + 34;
painter.etched_h_line(16, ramp_y - 10, W - 32, theme);
painter.text(
16,
ramp_y,
"size ramp — same baked geometry at every size:",
11.0,
theme.text,
);
let mut x = 150;
let top = ramp_y + 18;
for s in [16, 20, 24, 32, 40, 48, 64] {
let baked_box = Rect::new(x, top, s, s);
let rast_box = Rect::new(x + s + 12, top, s, s);
ramp.baked.draw(painter, baked_box);
draw_rasterized(painter, ramp.source, rast_box);
x += (s + 12) * 2 + 10;
}
painter.text(
16,
H - 22,
"Benchmark printed to the console — run with --release for real numbers.",
10.0,
theme.disabled_text,
);
}
}
fn benchmark() {
let sizes: [u32; 5] = [16, 32, 64, 128, 256];
println!(
"\n include_svg! vs runtime rasterization ({} icons, avg per op)",
ICONS.len()
);
if cfg!(debug_assertions) {
println!(" [debug build — numbers are rough; use --release to compare for real]");
}
println!(
" {:>6} {:>14} {:>14} {:>14} {:>9}",
"size", "baked fill", "parse+render", "render-only", "speedup",
);
println!(" {}", "-".repeat(72));
for &size in &sizes {
let budget: u64 = if cfg!(debug_assertions) {
400_000
} else {
6_000_000
};
let iters = (budget / (size as u64 * size as u64)).clamp(8, 4000) as u32;
let mut baked = Duration::ZERO;
let mut parse_render = Duration::ZERO;
let mut render_only = Duration::ZERO;
for icon in ICONS {
let mut buf = vec![Color::WHITE.0; (size * size) as usize];
let t = Instant::now();
for _ in 0..iters {
let mut p = Painter::new(
&mut buf,
size as i32,
size as i32,
1.0,
0,
0,
FontSet::default(),
);
icon.baked
.draw(&mut p, Rect::new(0, 0, size as i32, size as i32));
}
baked += t.elapsed();
let t = Instant::now();
for _ in 0..iters {
std::hint::black_box(rasterize_text(icon.source, size, size));
}
parse_render += t.elapsed();
let tree = usvg::Tree::from_str(icon.source, &usvg::Options::default()).unwrap();
let t = Instant::now();
for _ in 0..iters {
std::hint::black_box(rasterize_tree(&tree, size, size));
}
render_only += t.elapsed();
}
let ops = (ICONS.len() as u32 * iters) as f64;
let us = |d: Duration| d.as_secs_f64() * 1e6 / ops;
let (b, pr) = (us(baked), us(parse_render));
println!(
" {:>5}px {:>11.2} µs {:>11.2} µs {:>11.2} µs {:>7.0}×",
size,
b,
pr,
us(render_only),
pr / b,
);
}
fidelity();
println!(
"\n Plus: the baked path links no SVG crates at all — `resvg`/`usvg`/`tiny-skia`\n \
(a large dependency tree) stay entirely out of the binary.\n"
);
}
fn fidelity() {
let size = 64u32;
let mut total = 0.0;
let mut worst = (0.0, "");
for icon in ICONS {
let mut baked = vec![Color::WHITE.0; (size * size) as usize];
{
let mut p = Painter::new(
&mut baked,
size as i32,
size as i32,
1.0,
0,
0,
FontSet::default(),
);
icon.baked
.draw(&mut p, Rect::new(0, 0, size as i32, size as i32));
}
let rast = rasterize_text(icon.source, size, size);
let mut sum = 0u64;
for (&x, &y) in baked.iter().zip(rast.iter()) {
let (a, b) = (Color(x), Color(y));
sum += a.red().abs_diff(b.red()) as u64;
sum += a.green().abs_diff(b.green()) as u64;
sum += a.blue().abs_diff(b.blue()) as u64;
}
let mean = sum as f64 / (baked.len() * 3) as f64;
total += mean;
if mean > worst.0 {
worst = (mean, icon.name);
}
}
let avg = total / ICONS.len() as f64;
println!(
"\n fidelity vs resvg @ {size}px: mean Δ {:.2}/255 ({:.2}%); worst: {} at {:.2}/255",
avg,
avg / 255.0 * 100.0,
worst.1,
worst.0,
);
}