git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commite42a26c436f8aec59cf97c321b80c83652655781
parentd61a532c9f
authorLucas Galante <[email protected]>
date2026-08-04 20:47
feat: rasterize_svg — public SVG-to-RGBA helper, extracted from upload_icon

Same tree-parse + scale + un-premultiply body; upload_icon keeps the
upload/cache shell. Thread-safe (fontdb is a OnceLock static) so
services may rasterize off the UI thread — cce-files' preview uses it.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/lib.rs | 58 ++++++++++++++++++++++++++++++++++------------------------
 1 file changed, 34 insertions(+), 24 deletions(-)

diff --git a/src/lib.rs b/src/lib.rs
index adbb4c0..914dd6e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -69,36 +69,46 @@ pub fn upload_icon(name: &str, px: u32) -> Option<(u32, u32, u32)> {
     let loaded = (|| {
         let path = format!("{}/{name}.svg", icons_dir());
         let data = std::fs::read(&path).ok()?;
-        let opt = resvg::usvg::Options::default();
-        let fontdb = crate::widget::get_font_db();
-        let tree = resvg::usvg::Tree::from_data(&data, &opt, fontdb).ok()?;
-        let size = tree.size();
-        let (sw, sh) = (size.width().max(1.0), size.height().max(1.0));
-        let scale = px as f32 / sw.max(sh);
-        let w = (sw * scale).round().max(1.0) as u32;
-        let h = (sh * scale).round().max(1.0) as u32;
-        let mut pixmap = resvg::tiny_skia::Pixmap::new(w, h)?;
-        resvg::render(
-            &tree,
-            resvg::tiny_skia::Transform::from_scale(scale, scale),
-            &mut pixmap.as_mut(),
-        );
-        // tiny-skia pixels are premultiplied; the upload path takes straight RGBA.
-        let mut rgba = pixmap.take();
-        for p in rgba.chunks_exact_mut(4) {
-            let a = p[3] as f32 / 255.0;
-            if a > 0.0 {
-                p[0] = ((p[0] as f32 / a).min(255.0)) as u8;
-                p[1] = ((p[1] as f32 / a).min(255.0)) as u8;
-                p[2] = ((p[2] as f32 / a).min(255.0)) as u8;
-            }
-        }
+        let (rgba, w, h) = rasterize_svg(&data, px)?;
         Some((crate::vk::upload_rgba(rgba, w, h), w, h))
     })();
     cache.insert(key, loaded);
     loaded
 }
 
+/// Rasterize SVG bytes at `px` on the longer side: straight (un-premultiplied)
+/// RGBA8 pixels plus dimensions, ready for `vk::upload_rgba`. Text elements
+/// resolve through the shared fontdb (a thread-safe static), so callers may
+/// rasterize off the UI thread and upload later — cce-files' preview service
+/// does. `None` when the data is unparsable.
+pub fn rasterize_svg(data: &[u8], px: u32) -> Option<(Vec<u8>, u32, u32)> {
+    let opt = resvg::usvg::Options::default();
+    let fontdb = crate::widget::get_font_db();
+    let tree = resvg::usvg::Tree::from_data(data, &opt, fontdb).ok()?;
+    let size = tree.size();
+    let (sw, sh) = (size.width().max(1.0), size.height().max(1.0));
+    let scale = px as f32 / sw.max(sh);
+    let w = (sw * scale).round().max(1.0) as u32;
+    let h = (sh * scale).round().max(1.0) as u32;
+    let mut pixmap = resvg::tiny_skia::Pixmap::new(w, h)?;
+    resvg::render(
+        &tree,
+        resvg::tiny_skia::Transform::from_scale(scale, scale),
+        &mut pixmap.as_mut(),
+    );
+    // tiny-skia pixels are premultiplied; the upload path takes straight RGBA.
+    let mut rgba = pixmap.take();
+    for p in rgba.chunks_exact_mut(4) {
+        let a = p[3] as f32 / 255.0;
+        if a > 0.0 {
+            p[0] = ((p[0] as f32 / a).min(255.0)) as u8;
+            p[1] = ((p[1] as f32 / a).min(255.0)) as u8;
+            p[2] = ((p[2] as f32 / a).min(255.0)) as u8;
+        }
+    }
+    Some((rgba, w, h))
+}
+
 /// Build a glyphon `FontSystem` loaded with the bundled CCE fonts (house style).
 /// System fonts are loaded only if `$CCE_LOAD_SYSTEM_FONTS` is set. Configured
 /// custom fonts are validated with a warning if missing.