在 kuva 里,如果以 Skia(更准确说:tiny-skia,经由 resvg) 为例,它“把图形绘制出来”的路径是这样的:
kuva
tiny-skia
resvg
重要前提:kuva 并不是直接用 Skia 画“折线/散点/坐标轴”。它先生成 SVG 字符串,再把 SVG 交给 resvg,由 resvg 内部用 tiny-skia 栅格化成 PNG。
(A) 生成 Scene(与 SVG/Terminal/PDF 通用)
ScatterPlot/Heatmap/...
Vec<Plot>
Plot
Layout
render::render::render_multiple(plots, layout)
Scene
(B) 走 PNG 后端:Scene → SVG → resvg → tiny-skia → PNG bytes 这一段完全在 src/backend/png.rs:
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
tiny_skia::Pixmap::new(w, h)
resvg::render(tree, transform, pixmap)
pixmap.encode_png()
这一步在 src/backend/svg.rs:它遍历 Scene.elements(每个元素是一个 Primitive),把它们拼成 <circle> <text> <rect> <path> ... 之类的 SVG 标签。
src/backend/svg.rs
Scene.elements
Primitive
<circle> <text> <rect> <path> ...
示例片段(你能看到它对 Primitive::Circle、Primitive::Text 等做 match,然后输出 SVG):
Primitive::Circle
Primitive::Text
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 画出来”来说,关键点是:
draw_line/draw_circle
以散点图为例(概念上):
Primitive::Circle { cx, cy, r, fill }
Primitive::Line
Primitive::Path
在当前架构下:
Scene/Primitive
如果你想做“真正的 Skia 后端”,通常会是:
backend/skia.rs
render_scene(&Scene) -> image bytes
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)最合适?
src/render/render.rs
在 kuva 当前实现里(见 src/backend/png.rs),resvg 和 tiny-skia 主要是为 PNG(位图)输出服务的:
resvg::usvg::Tree::from_str(...)
resvg::render(...)
Pixmap
tiny_skia::Pixmap::new(w,h)
resvg::render(..., &mut pixmap)
简单理解:
是的:只输出 SVG 时,不需要 tiny-skia,也不需要 resvg。
原因是 kuva 的 SVG 输出路径是:
render::render::render_multiple(...)
backend::svg::SvgBackend.render_scene(&scene)
这条路径在 src/lib.rs 的 render_to_svg 就能看到:它直接调用 SvgBackend,没有 resvg/tiny-skia 参与。 而且在 Cargo.toml 中,PNG 功能是可选 feature:png = ["dep:resvg"],说明不开 png feature 时根本不会依赖/编译 resvg(也就不会用到 tiny-skia)。
src/lib.rs
render_to_svg
SvgBackend
resvg/tiny-skia
Cargo.toml
png = ["dep:resvg"]
png
因为 kuva 的设计是“统一后端”:
如果你愿意,我也可以帮你画一张更直观的“输出 SVG vs 输出 PNG”的分叉流程图。
不需要用到 Skia(tiny-skia)。kuva 的 PDF 输出逻辑和 PNG 类似:都是先把内部 Scene 变成 SVG 字符串,然后交给第三方库把 SVG 转成目标格式;但 PDF 用的是 svg2pdf,不是 resvg/tiny-skia。
svg2pdf
与 SVG/PNG 完全相同:
ScatterPlot
这部分在 src/lib.rs 的 render_to_pdf 能看到它先拿到 scene。
render_to_pdf
在 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)明确是:
PdfBackend
src/backend/pdf.rs
svg2pdf::usvg::Tree::from_str(...)
svg2pdf::to_pdf(...)
pub 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()) }
不会。
Scene -> SvgBackend -> SVG string
Scene -> SvgBackend -> SVG string -> resvg(usvg parse) -> tiny-skia Pixmap -> PNG bytes
Scene -> SvgBackend -> SVG string -> svg2pdf(usvg parse) -> PDF bytes
如果你想进一步确认“PDF 是否是真矢量(可缩放)还是某些元素会被栅格化”,需要看 svg2pdf 对你生成的 SVG 特性的支持范围(例如渐变、复杂 path、文本等)。你可以告诉我你最关心的图类型(比如 sankey/heatmap/contour),我可以帮你判断它们在 SvgBackend 里会生成哪些 SVG 特性,从而推测 PDF 转换的风险点。
Github开源生信云平台 DEMO