在 kuva 里,如果以 Skia(更准确说:tiny-skia,经由 resvg) 为例,它“把图形绘制出来”的路径是这样的:
重要前提:
kuva并不是直接用 Skia 画“折线/散点/坐标轴”。它先生成 SVG 字符串,再把 SVG 交给resvg,由resvg内部用tiny-skia栅格化成 PNG。
(A) 生成 Scene(与 SVG/Terminal/PDF 通用)
ScatterPlot/Heatmap/...Vec<Plot>(Plot enum)Layoutrender::render::render_multiple(plots, layout) 得到 Scene
Scene 本质上是:画布宽高 + 主题/字体 + 一堆“图元”(primitives)(B) 走 PNG 后端:Scene → SVG → resvg → tiny-skia → PNG bytes
这一段完全在 src/backend/png.rs:
pub fn render_scene(&self, scene: &Scene) -> Result<Vec<u8>, String> {
let svg_str = SvgBackend.render_scene(scene);
let mut fontdb = resvg::usvg::fontdb::Database::new();
fontdb.load_system_fonts();
let options = resvg::usvg::Options { ... };
let tree = resvg::usvg::Tree::from_str(&svg_str, &options)?;
let size = tree.size().to_int_size().scale_by(self.scale).expect(...);
let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width(), size.height()).ok_or(...)?;
let transform = resvg::tiny_skia::Transform::from_scale(self.scale, self.scale);
resvg::render(&tree, transform, &mut pixmap.as_mut());
pixmap.encode_png()
}
把它翻译成“绘制动作”就是:
SvgBackend.render_scene(scene)usvg::Tree::from_str(svg_str, options)
usvg 负责把 SVG 文本解析成内部矢量树tiny_skia::Pixmap::new(w, h)
resvg::render(tree, transform, pixmap)
resvg 遍历矢量树,把路径/文本/形状栅格化,实际绘制由 tiny-skia 执行pixmap.encode_png()这一步在 src/backend/svg.rs:它遍历 Scene.elements(每个元素是一个 Primitive),把它们拼成 <circle> <text> <rect> <path> ... 之类的 SVG 标签。
示例片段(你能看到它对 Primitive::Circle、Primitive::Text 等做 match,然后输出 SVG):
pub fn render_scene(&self, scene: &Scene) -> String {
...
for elem in &scene.elements {
match elem {
Primitive::Circle { cx, cy, r, fill } => {
svg.push_str(&format!(r#"<circle cx="{cx}" cy="{cy}" r="{r}" fill="{fill}" />"#));
}
Primitive::Text { x, y, content, size, anchor, rotate, bold } => {
...
}
...
}
}
...
}
所以对“用 Skia 画出来”来说,关键点是:
kuva 自己并不直接调用 Skia 的 draw_line/draw_circle 之类 APIScene + Primitive以散点图为例(概念上):
Primitive::Circle { cx, cy, r, fill }Primitive::Line / Primitive::PathPrimitive::Text在当前架构下:
Scene/Primitive 并调用 Skia API 的实现如果你想做“真正的 Skia 后端”,通常会是:
backend/skia.rsrender_scene(&Scene) -> image bytesPrimitive 映射到 tiny-skia 的绘制操作(PathBuilder、Paint、Stroke、fill_rect、文本需要额外字体排版支持等)如果你希望我更“落地”地说明:Scene 的 Primitive 都有哪些变体(Line/Rect/Path/Text/Circle…)以及它们在 SVG 里会生成什么标签、再如何被 resvg 转换为 tiny-skia 的绘制指令,我需要再把 src/render/render.rs 以及 Scene/Primitive 的定义位置一起展开。你希望我从哪个 plot 举例(scatter / heatmap / sankey)最合适?
在 kuva 当前实现里(见 src/backend/png.rs),resvg 和 tiny-skia 主要是为 PNG(位图)输出服务的:
resvg
resvg::usvg::Tree::from_str(...) 解析 SVG,再 resvg::render(...) 把它画出来。tiny-skia
Pixmap(像素缓冲区)以及把矢量图元实际“涂到像素上”的底层能力。tiny_skia::Pixmap::new(w,h) 分配像素缓冲,然后 resvg::render(..., &mut pixmap) 往里画,最后 pixmap.encode_png() 导出 PNG。简单理解:
是的:只输出 SVG 时,不需要 tiny-skia,也不需要 resvg。
原因是 kuva 的 SVG 输出路径是:
render::render::render_multiple(...) 生成 Scenebackend::svg::SvgBackend.render_scene(&scene) 直接把 Scene 转成 SVG 字符串这条路径在 src/lib.rs 的 render_to_svg 就能看到:它直接调用 SvgBackend,没有 resvg/tiny-skia 参与。
而且在 Cargo.toml 中,PNG 功能是可选 feature:png = ["dep:resvg"],说明不开 png feature 时根本不会依赖/编译 resvg(也就不会用到 tiny-skia)。
因为 kuva 的设计是“统一后端”:
Scene/Primitive如果你愿意,我也可以帮你画一张更直观的“输出 SVG vs 输出 PNG”的分叉流程图。
不需要用到 Skia(tiny-skia)。kuva 的 PDF 输出逻辑和 PNG 类似:都是先把内部 Scene 变成 SVG 字符串,然后交给第三方库把 SVG 转成目标格式;但 PDF 用的是 svg2pdf,不是 resvg/tiny-skia。
与 SVG/PNG 完全相同:
ScatterPlot)Vec<Plot>(Plot enum)Layoutrender::render::render_multiple(plots, layout) → 得到 Scene这部分在 src/lib.rs 的 render_to_pdf 能看到它先拿到 scene。
在 src/lib.rs 里:
pub fn render_to_pdf(plots: Vec<render::plots::Plot>, layout: render::layout::Layout) -> Result<Vec<u8>, String> {
let scene = render::render::render_multiple(plots, layout);
backend::pdf::PdfBackend.render_scene(&scene)
}
而 PdfBackend 的实现(src/backend/pdf.rs)明确是:
SvgBackend.render_scene(scene) 得到 SVG 字符串svg2pdf::usvg::Tree::from_str(...) 解析 SVGsvg2pdf::to_pdf(...) 输出 PDF bytespub fn render_scene(&self, scene: &Scene) -> Result<Vec<u8>, String> {
let svg_str = SvgBackend.render_scene(scene);
let mut fontdb = svg2pdf::usvg::fontdb::Database::new();
fontdb.load_system_fonts();
let options = svg2pdf::usvg::Options { ... };
let tree = svg2pdf::usvg::Tree::from_str(&svg_str, &options)
.map_err(|e| e.to_string())?;
svg2pdf::to_pdf(&tree, svg2pdf::ConversionOptions::default(), svg2pdf::PageOptions::default())
.map_err(|e| e.to_string())
}
不会。
resvg + tiny-skia(CPU 栅格化)把 SVG 画到像素,再编码 PNGsvg2pdf 把 SVG 的矢量内容转换成 PDF 矢量(不走像素画布,所以不需要 tiny-skia)Scene -> SvgBackend -> SVG stringScene -> SvgBackend -> SVG string -> resvg(usvg parse) -> tiny-skia Pixmap -> PNG bytesScene -> SvgBackend -> SVG string -> svg2pdf(usvg parse) -> PDF bytes如果你想进一步确认“PDF 是否是真矢量(可缩放)还是某些元素会被栅格化”,需要看 svg2pdf 对你生成的 SVG 特性的支持范围(例如渐变、复杂 path、文本等)。你可以告诉我你最关心的图类型(比如 sankey/heatmap/contour),我可以帮你判断它们在 SvgBackend 里会生成哪些 SVG 特性,从而推测 PDF 转换的风险点。