以skia为例,kuva是如何将图形绘制出来的

最后发布时间:2026-03-10 09:13:40 浏览量:

kuva 里,如果以 Skia(更准确说:tiny-skia,经由 resvg 为例,它“把图形绘制出来”的路径是这样的:

重要前提:kuva 并不是直接用 Skia 画“折线/散点/坐标轴”。它先生成 SVG 字符串,再把 SVG 交给 resvg,由 resvg 内部用 tiny-skia 栅格化成 PNG。


1) Skia 路径的总链路(从 Plot 到像素)

(A) 生成 Scene(与 SVG/Terminal/PDF 通用)

  1. 你构建 plot:ScatterPlot/Heatmap/...
  2. 转成统一的 Vec<Plot>Plot enum)
  3. 构建 Layout
  4. render::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()
}

把它翻译成“绘制动作”就是:

  1. Scene → SVG 字符串SvgBackend.render_scene(scene)
  2. SVG parseusvg::Tree::from_str(svg_str, options)
    • usvg 负责把 SVG 文本解析成内部矢量树
  3. 分配像素画布tiny_skia::Pixmap::new(w, h)
    • 这是最终会被画“像素”的 buffer
  4. 渲染resvg::render(tree, transform, pixmap)
    • resvg 遍历矢量树,把路径/文本/形状栅格化,实际绘制由 tiny-skia 执行
  5. 编码输出pixmap.encode_png()

2) 那么“图形”具体是怎么变成 SVG 的?

这一步在 src/backend/svg.rs:它遍历 Scene.elements(每个元素是一个 Primitive),把它们拼成 <circle> <text> <rect> <path> ... 之类的 SVG 标签。

示例片段(你能看到它对 Primitive::CirclePrimitive::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 之类 API
  • 它把所有东西先降级为一套中间表示:Scene + Primitive
  • 然后:
    • SVG:自己实现一个“Primitive → SVG 标签”后端
    • PNG(Skia):先复用 SVG 后端输出 SVG,再交给 resvg(tiny-skia)栅格化

3) 你可以把它理解成:Skia 是“最后一步的像素化引擎”

以散点图为例(概念上):

  • render 层先把散点、坐标轴、刻度、标题等全部转成 primitives:
    • 点:Primitive::Circle { cx, cy, r, fill }
    • 轴/网格:Primitive::Line / Primitive::Path
    • 文本:Primitive::Text
  • SVG backend 把它们写成 SVG
  • resvg 解析 SVG,再用 tiny-skia 逐像素绘制到 Pixmap

4) 如果你问的是“能否跳过 SVG,直接用 Skia 画?”

在当前架构下:

  • PNG 后端 = “SVG → resvg → tiny-skia”(已定型)
  • 并没有一个 “SkiaBackend” 直接消费 Scene/Primitive 并调用 Skia API 的实现

如果你想做“真正的 Skia 后端”,通常会是:

  • 新建 backend/skia.rs
  • 实现类似 render_scene(&Scene) -> image bytes
  • 在里面把 Primitive 映射到 tiny-skia 的绘制操作(PathBuilderPaintStrokefill_rect、文本需要额外字体排版支持等)
  • 优点:少一次 SVG 生成与解析;缺点:文本/路径/渐变等细节你要自己处理(resvg/usvg 已经替你做了大量复杂工作)

如果你希望我更“落地”地说明:Scene 的 Primitive 都有哪些变体(Line/Rect/Path/Text/Circle…)以及它们在 SVG 里会生成什么标签、再如何被 resvg 转换为 tiny-skia 的绘制指令,我需要再把 src/render/render.rs 以及 Scene/Primitive 的定义位置一起展开。你希望我从哪个 plot 举例(scatter / heatmap / sankey)最合适?

resvg 与 tiny-skia 各自的作用是什么?

kuva 当前实现里(见 src/backend/png.rs),resvgtiny-skia 主要是为 PNG(位图)输出服务的:

  • resvg

    • 角色:SVG 渲染器/栅格化器(renderer / rasterizer)
    • 做的事:把一段 SVG(字符串或解析后的树)转换成“画到像素画布上”的操作,并负责处理大量 SVG 细节(路径、变换、填充、描边、文本布局相关的一部分、裁剪、渐变等,具体支持度由 resvg 决定)。
    • 在代码里体现为:先用 resvg::usvg::Tree::from_str(...) 解析 SVG,再 resvg::render(...) 把它画出来。
  • tiny-skia

    • 角色:2D 像素画布与绘制引擎(CPU raster engine)
    • 做的事:提供 Pixmap(像素缓冲区)以及把矢量图元实际“涂到像素上”的底层能力。
    • 在代码里体现为:tiny_skia::Pixmap::new(w,h) 分配像素缓冲,然后 resvg::render(..., &mut pixmap) 往里画,最后 pixmap.encode_png() 导出 PNG。

简单理解:

  • resvg = “怎么把 SVG 画出来”
  • tiny-skia = “具体画到哪块像素内存里,并提供画笔/栅格化能力”
  • 在这个组合里通常是:resvg 负责解释 SVG,tiny-skia 负责落像素

如果只输出 SVG,是否不需要 tiny-skia?

是的:只输出 SVG 时,不需要 tiny-skia,也不需要 resvg。

原因是 kuva 的 SVG 输出路径是:

  1. render::render::render_multiple(...) 生成 Scene
  2. backend::svg::SvgBackend.render_scene(&scene) 直接把 Scene 转成 SVG 字符串

这条路径在 src/lib.rsrender_to_svg 就能看到:它直接调用 SvgBackend,没有 resvg/tiny-skia 参与。
而且在 Cargo.toml 中,PNG 功能是可选 feature:png = ["dep:resvg"],说明不开 png feature 时根本不会依赖/编译 resvg(也就不会用到 tiny-skia)。


额外补充:那为什么 PNG 要用 resvg/tiny-skia,而不是直接画?

因为 kuva 的设计是“统一后端”:

  • 先把图形归一为 Scene/Primitive
  • SVG backend:自己把 primitives 写成 SVG 标签
  • PNG backend:复用 SVG backend 生成 SVG,再交给成熟的 SVG 渲染器(resvg)去栅格化
    这样能少写一套“直接位图绘制”的复杂实现(尤其是文本、路径、渐变等细节)。

如果你愿意,我也可以帮你画一张更直观的“输出 SVG vs 输出 PNG”的分叉流程图。

不需要用到 Skia(tiny-skia)。kuvaPDF 输出逻辑和 PNG 类似:都是先把内部 Scene 变成 SVG 字符串,然后交给第三方库把 SVG 转成目标格式;但 PDF 用的是 svg2pdf,不是 resvg/tiny-skia


1) PDF 输出的完整链路(从 Plot 到 PDF bytes)

(A) 通用前半段:Plot → Layout → Scene

与 SVG/PNG 完全相同:

  1. 构建 plot struct(如 ScatterPlot
  2. 转成 Vec<Plot>Plot enum)
  3. 构建 Layout
  4. render::render::render_multiple(plots, layout) → 得到 Scene

这部分在 src/lib.rsrender_to_pdf 能看到它先拿到 scene。

(B) PDF 后半段:Scene → SVG → svg2pdf → PDF bytes

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)明确是:

  1. SvgBackend.render_scene(scene) 得到 SVG 字符串
  2. svg2pdf::usvg::Tree::from_str(...) 解析 SVG
  3. svg2pdf::to_pdf(...) 输出 PDF bytes
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())
}

2) 结论:PDF 会用到 Skia 吗?

不会。

  • PNG 输出resvg + tiny-skia(CPU 栅格化)把 SVG 画到像素,再编码 PNG
  • PDF 输出svg2pdf 把 SVG 的矢量内容转换成 PDF 矢量(不走像素画布,所以不需要 tiny-skia)

3) 关键差异(SVG / PNG / PDF 三条路对比)

  • SVGScene -> SvgBackend -> SVG string
  • PNGScene -> SvgBackend -> SVG string -> resvg(usvg parse) -> tiny-skia Pixmap -> PNG bytes
  • PDFScene -> SvgBackend -> SVG string -> svg2pdf(usvg parse) -> PDF bytes

如果你想进一步确认“PDF 是否是真矢量(可缩放)还是某些元素会被栅格化”,需要看 svg2pdf 对你生成的 SVG 特性的支持范围(例如渐变、复杂 path、文本等)。你可以告诉我你最关心的图类型(比如 sankey/heatmap/contour),我可以帮你判断它们在 SvgBackend 里会生成哪些 SVG 特性,从而推测 PDF 转换的风险点。