Skip to main content

svg_renderer/
renderer.rs

1use std::path::PathBuf;
2
3#[cfg(feature = "vulkan-backend")]
4use std::sync::Arc;
5
6#[cfg(feature = "vulkan-backend")]
7use ash::vk::Handle;
8#[cfg(feature = "vulkan-backend")]
9use skia_safe::gpu::{self, direct_contexts, vk as skia_vk};
10use skia_safe::{
11    AlphaType, ColorType, FontMgr, IPoint, ImageInfo, Size, jpeg_encoder, png_encoder,
12    resources::NativeResourceProvider, surfaces, svg::Dom, webp_encoder,
13};
14
15#[cfg(feature = "vulkan-backend")]
16use crate::VulkanState;
17use crate::{
18    CachedResourceProvider, ImageData, JpegOptions, RenderOptions, SvgRenderError, WebpOptions,
19    options::MAX_RENDER_BYTES,
20};
21
22/// Rendering backend kind.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum RenderBackend {
25    /// Skia raster (CPU) backend.
26    Cpu,
27    /// Skia Vulkan (GPU) backend.
28    Vulkan,
29}
30
31/// CPU-based SVG renderer using Skia's raster backend.
32///
33/// Suitable for most environments; no GPU required. Each instance keeps
34/// an internal readback buffer that is reused across calls to reduce
35/// allocation overhead.
36pub struct CpuSvgRenderer {
37    resource_provider: CachedResourceProvider,
38    /// Reusable readback buffer to avoid per-call allocations.
39    readback_buffer: Vec<u8>,
40}
41
42impl CpuSvgRenderer {
43    /// Creates a new CPU renderer with a default font manager.
44    pub fn new() -> Result<Self, SvgRenderError> {
45        Ok(Self {
46            resource_provider: CachedResourceProvider::new(FontMgr::default()),
47            readback_buffer: Vec::new(),
48        })
49    }
50
51    /// Appends a directory to the resource search path.
52    ///
53    /// Resources (fonts, images referenced by the SVG) are looked up in
54    /// all registered directories and, as a fallback, via HTTP(S).
55    pub fn add_resource_search_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
56        self.resource_provider.add_search_dir(dir);
57        self
58    }
59
60    /// Replaces the resource search path with the given directories.
61    pub fn set_resource_search_dirs<I, P>(&mut self, dirs: I) -> &mut Self
62    where
63        I: IntoIterator<Item = P>,
64        P: Into<PathBuf>,
65    {
66        self.resource_provider.set_search_dirs(dirs);
67        self
68    }
69
70    /// Renders an SVG into raw RGBA pixel data.
71    pub fn render_svg(
72        &mut self,
73        svg: impl AsRef<[u8]>,
74        options: &RenderOptions,
75    ) -> Result<ImageData, SvgRenderError> {
76        let mut surface = self.render_surface(svg, options)?;
77        read_surface_pixels(&mut surface, options, &mut self.readback_buffer)
78    }
79
80    /// Renders an SVG and encodes the result as PNG.
81    pub fn render_svg_to_png(
82        &mut self,
83        svg: impl AsRef<[u8]>,
84        options: &RenderOptions,
85    ) -> Result<Vec<u8>, SvgRenderError> {
86        let mut surface = self.render_surface(svg, options)?;
87        let image = surface.image_snapshot();
88        let data = png_encoder::encode_image(None, &image, &png_encoder::Options::default())
89            .ok_or(SvgRenderError::PngEncode)?;
90
91        Ok(data.as_bytes().to_vec())
92    }
93
94    /// Renders an SVG and encodes the result as JPEG.
95    pub fn render_svg_to_jpeg(
96        &mut self,
97        svg: impl AsRef<[u8]>,
98        options: &RenderOptions,
99        jpeg_options: JpegOptions,
100    ) -> Result<Vec<u8>, SvgRenderError> {
101        let mut surface = self.render_surface(svg, options)?;
102        let image = surface.image_snapshot();
103        let data = jpeg_encoder::encode_image(None, &image, &jpeg_options.into())
104            .ok_or(SvgRenderError::JpegEncode)?;
105
106        Ok(data.as_bytes().to_vec())
107    }
108
109    /// Renders an SVG and encodes the result as WebP.
110    pub fn render_svg_to_webp(
111        &mut self,
112        svg: impl AsRef<[u8]>,
113        options: &RenderOptions,
114        webp_options: WebpOptions,
115    ) -> Result<Vec<u8>, SvgRenderError> {
116        let mut surface = self.render_surface(svg, options)?;
117        let image = surface.image_snapshot();
118        let data = webp_encoder::encode_image(None, &image, &webp_options.into())
119            .ok_or(SvgRenderError::WebpEncode)?;
120
121        Ok(data.as_bytes().to_vec())
122    }
123
124    /// Internal: creates a raster surface, parses the SVG, renders onto it.
125    fn render_surface(
126        &mut self,
127        svg: impl AsRef<[u8]>,
128        options: &RenderOptions,
129    ) -> Result<skia_safe::Surface, SvgRenderError> {
130        let (width, height) = options.size.as_i32_pair();
131        let info = rgba_image_info(width, height);
132        let mut surface =
133            surfaces::raster(&info, None, None).ok_or(SvgRenderError::RenderTarget)?;
134        render_dom(surface.canvas(), svg, options, &self.resource_provider)?;
135        Ok(surface)
136    }
137}
138
139/// Vulkan GPU-accelerated SVG renderer using Skia's Vulkan backend.
140///
141/// Requires the `vulkan-backend` feature. Falls back to CPU if Vulkan
142/// initialization fails (see [`SvgRenderer::new`]).
143#[cfg(feature = "vulkan-backend")]
144pub struct VulkanSvgRenderer {
145    vulkan: Arc<VulkanState>,
146    context: gpu::DirectContext,
147    resource_provider: CachedResourceProvider,
148    readback_buffer: Vec<u8>,
149}
150
151#[cfg(feature = "vulkan-backend")]
152impl VulkanSvgRenderer {
153    /// Creates a Vulkan renderer: loads the Vulkan library, enumerates
154    /// devices, picks a graphics-capable queue, and wraps it in a Skia
155    /// direct context.
156    pub fn new() -> Result<Self, SvgRenderError> {
157        let vulkan = Arc::new(VulkanState::new()?);
158        let get_proc_vulkan = Arc::clone(&vulkan);
159        let get_proc = move |proc| get_proc_vulkan.get_proc(proc);
160
161        let backend_context = unsafe {
162            skia_vk::BackendContext::new_builder(
163                vulkan.instance.handle().as_raw() as skia_vk::Instance,
164                vulkan.physical_device.as_raw() as skia_vk::PhysicalDevice,
165                vulkan.device.handle().as_raw() as skia_vk::Device,
166                (
167                    vulkan.queue.as_raw() as skia_vk::Queue,
168                    vulkan.queue_family_index as usize,
169                ),
170                &get_proc,
171                Some(skia_vk::Version::new(1, 1, 0)),
172            )
173            .build()
174        };
175
176        let context = direct_contexts::make_vulkan(&backend_context, None)
177            .ok_or(SvgRenderError::SkiaContext)?;
178        let resource_provider = CachedResourceProvider::new(FontMgr::default());
179
180        Ok(Self {
181            vulkan,
182            context,
183            resource_provider,
184            readback_buffer: Vec::new(),
185        })
186    }
187
188    /// Appends a directory to the resource search path.
189    pub fn add_resource_search_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
190        self.resource_provider.add_search_dir(dir);
191        self
192    }
193
194    /// Replaces the resource search path with the given directories.
195    pub fn set_resource_search_dirs<I, P>(&mut self, dirs: I) -> &mut Self
196    where
197        I: IntoIterator<Item = P>,
198        P: Into<PathBuf>,
199    {
200        self.resource_provider.set_search_dirs(dirs);
201        self
202    }
203
204    /// Renders an SVG into raw RGBA pixel data.
205    ///
206    /// Flushes the GPU command buffer before readback.
207    pub fn render_svg(
208        &mut self,
209        svg: impl AsRef<[u8]>,
210        options: &RenderOptions,
211    ) -> Result<ImageData, SvgRenderError> {
212        let mut surface = self.render_surface(svg, options)?;
213        self.context.flush_and_submit();
214        read_surface_pixels(&mut surface, options, &mut self.readback_buffer)
215    }
216
217    /// Renders an SVG and encodes the result as PNG.
218    ///
219    /// Flushes the GPU command buffer before encoding.
220    pub fn render_svg_to_png(
221        &mut self,
222        svg: impl AsRef<[u8]>,
223        options: &RenderOptions,
224    ) -> Result<Vec<u8>, SvgRenderError> {
225        let mut surface = self.render_surface(svg, options)?;
226        self.context.flush_and_submit();
227        let image = surface.image_snapshot();
228        let data = png_encoder::encode_image(
229            Some(&mut self.context),
230            &image,
231            &png_encoder::Options::default(),
232        )
233        .ok_or(SvgRenderError::PngEncode)?;
234
235        Ok(data.as_bytes().to_vec())
236    }
237
238    /// Renders an SVG and encodes the result as JPEG.
239    pub fn render_svg_to_jpeg(
240        &mut self,
241        svg: impl AsRef<[u8]>,
242        options: &RenderOptions,
243        jpeg_options: JpegOptions,
244    ) -> Result<Vec<u8>, SvgRenderError> {
245        let mut surface = self.render_surface(svg, options)?;
246        self.context.flush_and_submit();
247        let image = surface.image_snapshot();
248        let data =
249            jpeg_encoder::encode_image(Some(&mut self.context), &image, &jpeg_options.into())
250                .ok_or(SvgRenderError::JpegEncode)?;
251
252        Ok(data.as_bytes().to_vec())
253    }
254
255    /// Renders an SVG and encodes the result as WebP.
256    pub fn render_svg_to_webp(
257        &mut self,
258        svg: impl AsRef<[u8]>,
259        options: &RenderOptions,
260        webp_options: WebpOptions,
261    ) -> Result<Vec<u8>, SvgRenderError> {
262        let mut surface = self.render_surface(svg, options)?;
263        self.context.flush_and_submit();
264        let image = surface.image_snapshot();
265        let data =
266            webp_encoder::encode_image(Some(&mut self.context), &image, &webp_options.into())
267                .ok_or(SvgRenderError::WebpEncode)?;
268
269        Ok(data.as_bytes().to_vec())
270    }
271
272    /// Internal: creates a GPU-backed surface, parses the SVG, renders it.
273    fn render_surface(
274        &mut self,
275        svg: impl AsRef<[u8]>,
276        options: &RenderOptions,
277    ) -> Result<skia_safe::Surface, SvgRenderError> {
278        let (width, height) = options.size.as_i32_pair();
279        let info = rgba_image_info(width, height);
280        let mut surface = gpu::surfaces::render_target(
281            &mut self.context,
282            gpu::Budgeted::No,
283            &info,
284            options.sample_count,
285            gpu::SurfaceOrigin::TopLeft,
286            None,
287            false,
288            false,
289        )
290        .ok_or(SvgRenderError::RenderTarget)?;
291        render_dom(surface.canvas(), svg, options, &self.resource_provider)?;
292        Ok(surface)
293    }
294}
295
296#[cfg(feature = "vulkan-backend")]
297impl Drop for VulkanSvgRenderer {
298    fn drop(&mut self) {
299        // Abandon the Skia context so it doesn't try to destroy Vulkan
300        // resources that the VulkanState drop will handle.
301        self.context.abandon();
302        // Keep a reference alive via strong_count read to prevent
303        // compiler from optimizing away the Arc.
304        let _ = Arc::strong_count(&self.vulkan);
305    }
306}
307
308/// Auto-selecting SVG renderer.
309///
310/// Tries Vulkan first (when the `vulkan-backend` feature is enabled);
311/// falls back to CPU on any error during Vulkan init. This makes it a
312/// safe default for most use cases.
313pub struct SvgRenderer {
314    renderer: SvgRendererBackend,
315}
316
317enum SvgRendererBackend {
318    Cpu(CpuSvgRenderer),
319    #[cfg(feature = "vulkan-backend")]
320    Vulkan(VulkanSvgRenderer),
321}
322
323impl SvgRenderer {
324    /// Creates a renderer, preferring Vulkan over CPU.
325    pub fn new() -> Result<Self, SvgRenderError> {
326        #[cfg(feature = "vulkan-backend")]
327        if let Ok(renderer) = VulkanSvgRenderer::new() {
328            return Ok(Self {
329                renderer: SvgRendererBackend::Vulkan(renderer),
330            });
331        }
332
333        Ok(Self {
334            renderer: SvgRendererBackend::Cpu(CpuSvgRenderer::new()?),
335        })
336    }
337
338    /// Returns which backend is currently in use.
339    pub fn backend(&self) -> RenderBackend {
340        match &self.renderer {
341            SvgRendererBackend::Cpu(_) => RenderBackend::Cpu,
342            #[cfg(feature = "vulkan-backend")]
343            SvgRendererBackend::Vulkan(_) => RenderBackend::Vulkan,
344        }
345    }
346
347    /// Appends a directory to the resource search path.
348    pub fn add_resource_search_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
349        match &mut self.renderer {
350            SvgRendererBackend::Cpu(renderer) => {
351                renderer.add_resource_search_dir(dir);
352            }
353            #[cfg(feature = "vulkan-backend")]
354            SvgRendererBackend::Vulkan(renderer) => {
355                renderer.add_resource_search_dir(dir);
356            }
357        }
358        self
359    }
360
361    /// Replaces the resource search path with the given directories.
362    pub fn set_resource_search_dirs<I, P>(&mut self, dirs: I) -> &mut Self
363    where
364        I: IntoIterator<Item = P>,
365        P: Into<PathBuf>,
366    {
367        match &mut self.renderer {
368            SvgRendererBackend::Cpu(renderer) => {
369                renderer.set_resource_search_dirs(dirs);
370            }
371            #[cfg(feature = "vulkan-backend")]
372            SvgRendererBackend::Vulkan(renderer) => {
373                renderer.set_resource_search_dirs(dirs);
374            }
375        }
376        self
377    }
378
379    /// Renders an SVG into raw RGBA pixel data.
380    pub fn render_svg(
381        &mut self,
382        svg: impl AsRef<[u8]>,
383        options: &RenderOptions,
384    ) -> Result<ImageData, SvgRenderError> {
385        match &mut self.renderer {
386            SvgRendererBackend::Cpu(renderer) => renderer.render_svg(svg, options),
387            #[cfg(feature = "vulkan-backend")]
388            SvgRendererBackend::Vulkan(renderer) => renderer.render_svg(svg, options),
389        }
390    }
391
392    /// Renders an SVG and encodes the result as PNG.
393    pub fn render_svg_to_png(
394        &mut self,
395        svg: impl AsRef<[u8]>,
396        options: &RenderOptions,
397    ) -> Result<Vec<u8>, SvgRenderError> {
398        match &mut self.renderer {
399            SvgRendererBackend::Cpu(renderer) => renderer.render_svg_to_png(svg, options),
400            #[cfg(feature = "vulkan-backend")]
401            SvgRendererBackend::Vulkan(renderer) => renderer.render_svg_to_png(svg, options),
402        }
403    }
404
405    /// Renders an SVG and encodes the result as JPEG.
406    pub fn render_svg_to_jpeg(
407        &mut self,
408        svg: impl AsRef<[u8]>,
409        options: &RenderOptions,
410        jpeg_options: JpegOptions,
411    ) -> Result<Vec<u8>, SvgRenderError> {
412        match &mut self.renderer {
413            SvgRendererBackend::Cpu(renderer) => {
414                renderer.render_svg_to_jpeg(svg, options, jpeg_options)
415            }
416            #[cfg(feature = "vulkan-backend")]
417            SvgRendererBackend::Vulkan(renderer) => {
418                renderer.render_svg_to_jpeg(svg, options, jpeg_options)
419            }
420        }
421    }
422
423    /// Renders an SVG and encodes the result as WebP.
424    pub fn render_svg_to_webp(
425        &mut self,
426        svg: impl AsRef<[u8]>,
427        options: &RenderOptions,
428        webp_options: WebpOptions,
429    ) -> Result<Vec<u8>, SvgRenderError> {
430        match &mut self.renderer {
431            SvgRendererBackend::Cpu(renderer) => {
432                renderer.render_svg_to_webp(svg, options, webp_options)
433            }
434            #[cfg(feature = "vulkan-backend")]
435            SvgRendererBackend::Vulkan(renderer) => {
436                renderer.render_svg_to_webp(svg, options, webp_options)
437            }
438        }
439    }
440}
441
442/// Creates an RGBA8888 premultiplied-alpha image info for the given dimensions.
443fn rgba_image_info(width: i32, height: i32) -> ImageInfo {
444    ImageInfo::new(
445        (width, height),
446        ColorType::RGBA8888,
447        AlphaType::Premul,
448        None,
449    )
450}
451
452/// Parses the SVG data, sets the container size, and renders onto `canvas`.
453fn render_dom(
454    canvas: &skia_safe::Canvas,
455    svg: impl AsRef<[u8]>,
456    options: &RenderOptions,
457    resource_provider: &CachedResourceProvider,
458) -> Result<(), SvgRenderError> {
459    let (width, height) = options.size.as_i32_pair();
460    let resource_provider: NativeResourceProvider = resource_provider.clone().into();
461    let mut dom =
462        Dom::from_bytes(svg.as_ref(), resource_provider).map_err(|_| SvgRenderError::SvgParse)?;
463    dom.set_container_size(Size::new(width as f32, height as f32));
464
465    canvas.clear(options.clear_color);
466    dom.render(canvas);
467    Ok(())
468}
469
470/// Reads back pixels from the surface into an [`ImageData`].
471fn read_surface_pixels(
472    surface: &mut skia_safe::Surface,
473    options: &RenderOptions,
474    readback_buffer: &mut Vec<u8>,
475) -> Result<ImageData, SvgRenderError> {
476    let (width, height) = options.size.as_i32_pair();
477    let info = rgba_image_info(width, height);
478    let row_bytes = (width as usize)
479        .checked_mul(4)
480        .ok_or(SvgRenderError::InvalidSize {
481            width: options.size.width,
482            height: options.size.height,
483        })?;
484    let byte_len = row_bytes
485        .checked_mul(height as usize)
486        .ok_or(SvgRenderError::InvalidSize {
487            width: options.size.width,
488            height: options.size.height,
489        })?;
490    if byte_len > MAX_RENDER_BYTES {
491        return Err(SvgRenderError::InvalidSize {
492            width: options.size.width,
493            height: options.size.height,
494        });
495    }
496    readback_buffer.resize(byte_len, 0);
497
498    if !surface.read_pixels(&info, readback_buffer, row_bytes, IPoint::new(0, 0)) {
499        return Err(SvgRenderError::ReadPixels);
500    }
501
502    Ok(ImageData {
503        width: options.size.width,
504        height: options.size.height,
505        row_bytes,
506        rgba: readback_buffer.clone(),
507    })
508}
509
510/// Convenience: creates an [`SvgRenderer`], renders the SVG to raw RGBA.
511pub fn render_svg(
512    svg: impl AsRef<[u8]>,
513    options: &RenderOptions,
514) -> Result<ImageData, SvgRenderError> {
515    SvgRenderer::new()?.render_svg(svg, options)
516}
517
518/// Convenience: creates an [`SvgRenderer`], renders the SVG, encodes as PNG.
519pub fn render_svg_to_png(
520    svg: impl AsRef<[u8]>,
521    options: &RenderOptions,
522) -> Result<Vec<u8>, SvgRenderError> {
523    SvgRenderer::new()?.render_svg_to_png(svg, options)
524}
525
526/// Convenience: creates an [`SvgRenderer`], renders the SVG, encodes as JPEG.
527pub fn render_svg_to_jpeg(
528    svg: impl AsRef<[u8]>,
529    options: &RenderOptions,
530    jpeg_options: JpegOptions,
531) -> Result<Vec<u8>, SvgRenderError> {
532    SvgRenderer::new()?.render_svg_to_jpeg(svg, options, jpeg_options)
533}
534
535/// Convenience: creates an [`SvgRenderer`], renders the SVG, encodes as WebP.
536pub fn render_svg_to_webp(
537    svg: impl AsRef<[u8]>,
538    options: &RenderOptions,
539    webp_options: WebpOptions,
540) -> Result<Vec<u8>, SvgRenderError> {
541    SvgRenderer::new()?.render_svg_to_webp(svg, options, webp_options)
542}