Skip to main content

uzor_icon/
lib.rs

1//! Build-time icon utilities for uzor apps.
2//!
3//! Convert an SVG asset into PNG (single size) or `.ico` (multi-size Windows icon)
4//! during `build.rs`. Designed to be called from a build script:
5//!
6//! ```no_run
7//! // In build.rs:
8//! fn main() {
9//!     uzor_icon::svg_to_png("assets/logo.svg", "icon-32.png", 32).unwrap();
10//!     uzor_icon::svg_to_ico("assets/logo.svg", "icon.ico", &[16, 32, 48, 256]).unwrap();
11//! }
12//! ```
13
14use std::fs;
15use std::io::Cursor;
16use std::path::Path;
17
18use image::codecs::ico::{IcoEncoder, IcoFrame};
19use image::codecs::png::PngEncoder;
20use image::{ColorType, ImageEncoder};
21use resvg::usvg::{Options, Tree};
22use tiny_skia::{Pixmap, Transform};
23
24// ── IconError ─────────────────────────────────────────────────────────────────
25
26/// Error type for icon conversion operations.
27#[derive(Debug)]
28pub enum IconError {
29    /// Underlying I/O failure (read source or write destination).
30    Io(std::io::Error),
31    /// SVG parse / tree construction failed.
32    Svg(String),
33    /// Rasterisation failed (e.g. zero-size pixmap).
34    Render(String),
35    /// Image encoding failed.
36    Encode(String),
37}
38
39impl std::fmt::Display for IconError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            IconError::Io(e) => write!(f, "icon I/O error: {e}"),
43            IconError::Svg(s) => write!(f, "SVG error: {s}"),
44            IconError::Render(s) => write!(f, "render error: {s}"),
45            IconError::Encode(s) => write!(f, "encode error: {s}"),
46        }
47    }
48}
49
50impl std::error::Error for IconError {
51    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
52        match self {
53            IconError::Io(e) => Some(e),
54            _ => None,
55        }
56    }
57}
58
59impl From<std::io::Error> for IconError {
60    fn from(e: std::io::Error) -> Self {
61        IconError::Io(e)
62    }
63}
64
65/// Alias for `Result<T, IconError>`.
66pub type Result<T> = std::result::Result<T, IconError>;
67
68// ── Internal helpers ──────────────────────────────────────────────────────────
69
70/// Parse SVG bytes into a `resvg` tree.
71fn parse_tree(svg: &[u8]) -> Result<Tree> {
72    Tree::from_data(svg, &Options::default())
73        .map_err(|e| IconError::Svg(e.to_string()))
74}
75
76/// Render `tree` into an RGBA `Pixmap` of `size × size` pixels.
77fn render_to_pixmap(tree: &Tree, size: u32) -> Result<Pixmap> {
78    let mut pixmap = Pixmap::new(size, size)
79        .ok_or_else(|| IconError::Render(format!("cannot create {size}×{size} pixmap")))?;
80
81    let svg_size = tree.size();
82    let sx = size as f32 / svg_size.width();
83    let sy = size as f32 / svg_size.height();
84    let transform = Transform::from_scale(sx, sy);
85
86    resvg::render(tree, transform, &mut pixmap.as_mut());
87    Ok(pixmap)
88}
89
90/// Encode a `Pixmap`'s RGBA data as PNG bytes.
91fn pixmap_to_png_bytes(pixmap: &Pixmap) -> Result<Vec<u8>> {
92    let mut out = Vec::new();
93    let encoder = PngEncoder::new(Cursor::new(&mut out));
94    encoder
95        .write_image(
96            pixmap.data(),
97            pixmap.width(),
98            pixmap.height(),
99            ColorType::Rgba8.into(),
100        )
101        .map_err(|e| IconError::Encode(e.to_string()))?;
102    Ok(out)
103}
104
105// ── Public API ────────────────────────────────────────────────────────────────
106
107/// Render an SVG file to a single-size PNG file.
108///
109/// # Arguments
110///
111/// * `svg_path` — path to the source `.svg` file.
112/// * `out_path` — path where the `.png` will be written (created or overwritten).
113/// * `size` — output image size in pixels (square: `size × size`).
114///
115/// # Errors
116///
117/// Returns [`IconError`] on I/O failure, SVG parse error, rasterisation error,
118/// or PNG encoding failure.
119pub fn svg_to_png(
120    svg_path: impl AsRef<Path>,
121    out_path: impl AsRef<Path>,
122    size: u32,
123) -> Result<()> {
124    let svg = fs::read(svg_path)?;
125    let png = svg_bytes_to_png_bytes(&svg, size)?;
126    fs::write(out_path, png)?;
127    Ok(())
128}
129
130/// Render an SVG file to a multi-size Windows `.ico` file.
131///
132/// # Arguments
133///
134/// * `svg_path` — path to the source `.svg` file.
135/// * `out_path` — path where the `.ico` will be written (created or overwritten).
136/// * `sizes`    — pixel sizes to embed (e.g. `&[16, 32, 48, 256]`).
137///
138/// # Errors
139///
140/// Returns [`IconError`] on any failure during read, render, or encode.
141pub fn svg_to_ico(
142    svg_path: impl AsRef<Path>,
143    out_path: impl AsRef<Path>,
144    sizes: &[u32],
145) -> Result<()> {
146    let svg = fs::read(svg_path)?;
147    let ico = svg_bytes_to_ico_bytes(&svg, sizes)?;
148    fs::write(out_path, ico)?;
149    Ok(())
150}
151
152/// Convert raw SVG bytes to PNG bytes at the given size.
153///
154/// In-memory variant of [`svg_to_png`] — no filesystem I/O.
155pub fn svg_bytes_to_png_bytes(svg: &[u8], size: u32) -> Result<Vec<u8>> {
156    let tree = parse_tree(svg)?;
157    let pixmap = render_to_pixmap(&tree, size)?;
158    pixmap_to_png_bytes(&pixmap)
159}
160
161/// Convert raw SVG bytes to a flat RGBA buffer of `size × size × 4` bytes.
162///
163/// Useful for passing directly to [`winit::window::Icon::from_rgba`] or
164/// [`tray_icon::Icon::from_rgba`].
165pub fn svg_bytes_to_rgba(svg: &[u8], size: u32) -> Result<Vec<u8>> {
166    let tree = parse_tree(svg)?;
167    let pixmap = render_to_pixmap(&tree, size)?;
168    Ok(pixmap.data().to_vec())
169}
170
171/// Convert raw SVG bytes to a multi-size `.ico` binary blob.
172///
173/// In-memory variant of [`svg_to_ico`] — no filesystem I/O.
174pub fn svg_bytes_to_ico_bytes(svg: &[u8], sizes: &[u32]) -> Result<Vec<u8>> {
175    let tree = parse_tree(svg)?;
176
177    // Render each size and collect as PNG-encoded frames.
178    let mut png_frames: Vec<Vec<u8>> = Vec::with_capacity(sizes.len());
179    for &sz in sizes {
180        let pixmap = render_to_pixmap(&tree, sz)?;
181        let png = pixmap_to_png_bytes(&pixmap)?;
182        png_frames.push(png);
183    }
184
185    // Build ICO from PNG frames.
186    let frames: std::result::Result<Vec<IcoFrame<'_>>, _> = png_frames
187        .iter()
188        .zip(sizes.iter())
189        .map(|(png, &sz)| IcoFrame::as_png(png, sz, sz, ColorType::Rgba8.into()))
190        .collect();
191    let frames = frames.map_err(|e| IconError::Encode(e.to_string()))?;
192
193    let mut out = Vec::new();
194    let encoder = IcoEncoder::new(Cursor::new(&mut out));
195    encoder
196        .encode_images(&frames)
197        .map_err(|e| IconError::Encode(e.to_string()))?;
198
199    Ok(out)
200}