imgico/
lib.rs

1use base64::{engine::general_purpose, Engine as _};
2use image::ImageOutputFormat;
3use std::io::{Cursor, Write};
4use wasm_bindgen::prelude::*;
5
6#[wasm_bindgen]
7pub fn set_panic_hook() {
8    console_error_panic_hook::set_once();
9}
10
11const DEFAULT_SIZES: [u32; 6] = [16, 32, 48, 64, 128, 256];
12
13pub fn imgico_core(input: &[u8], sizes: Option<Vec<u32>>) -> Result<Vec<u8>, String> {
14    let sizes = sizes.unwrap_or_else(|| DEFAULT_SIZES.to_vec());
15    let img = image::load_from_memory(input).map_err(|e| format!("Failed to load image: {}", e))?;
16
17    let mut images = Vec::new();
18
19    for size in sizes {
20        if size < 1 || size > 256 {
21            return Err(format!(
22                "Invalid icon size: {}. Size must be between 1 and 256.",
23                size
24            ));
25        }
26
27        // Make square by cropping from center
28        let (width, height) = (img.width(), img.height());
29        let min_dim = width.min(height);
30        let x_offset = (width - min_dim) / 2;
31        let y_offset = (height - min_dim) / 2;
32
33        let square_img = img.crop_imm(x_offset, y_offset, min_dim, min_dim);
34
35        // Resize to exact dimensions (no aspect ratio preservation)
36        let resized = square_img.resize_exact(size, size, image::imageops::FilterType::Lanczos3);
37
38        let mut buffer = Cursor::new(Vec::new());
39        resized
40            .write_to(&mut buffer, ImageOutputFormat::Png)
41            .map_err(|e| format!("Failed to write PNG: {}", e))?;
42
43        images.push((buffer.into_inner(), size));
44    }
45
46    // Create ICO header
47    let mut ico_data = Vec::new();
48
49    // Header
50    ico_data.write_all(&0u16.to_le_bytes()).unwrap(); // Reserved
51    ico_data.write_all(&1u16.to_le_bytes()).unwrap(); // Type (1 = ICO)
52    ico_data
53        .write_all(&(images.len() as u16).to_le_bytes())
54        .unwrap(); // Count
55
56    let directory_size = 16 * images.len();
57    let mut offset = 6 + directory_size;
58
59    // Directory Entries
60    for (buffer, size) in &images {
61        let dim = if *size >= 256 { 0 } else { *size as u8 };
62        ico_data.write_all(&[dim]).unwrap(); // Width
63        ico_data.write_all(&[dim]).unwrap(); // Height
64        ico_data.write_all(&[0]).unwrap(); // Palette count
65        ico_data.write_all(&[0]).unwrap(); // Reserved
66        ico_data.write_all(&1u16.to_le_bytes()).unwrap(); // Color planes
67        ico_data.write_all(&32u16.to_le_bytes()).unwrap(); // Bits per pixel
68        ico_data
69            .write_all(&(buffer.len() as u32).to_le_bytes())
70            .unwrap(); // Size
71        ico_data.write_all(&(offset as u32).to_le_bytes()).unwrap(); // Offset
72
73        offset += buffer.len();
74    }
75
76    // Image Data
77    for (buffer, _) in images {
78        ico_data.write_all(&buffer).unwrap();
79    }
80
81    Ok(ico_data)
82}
83
84#[wasm_bindgen]
85pub fn imgico(input: &[u8], sizes: Option<Vec<u32>>) -> Result<Vec<u8>, JsValue> {
86    imgico_core(input, sizes).map_err(|e| JsValue::from_str(&e))
87}
88
89pub fn imgsvg_core(input: &[u8], size: Option<u32>) -> Result<Vec<u8>, String> {
90    let img = image::load_from_memory(input).map_err(|e| format!("Failed to load image: {}", e))?;
91
92    let final_img = if let Some(s) = size {
93        // Make square by cropping from center
94        let (width, height) = (img.width(), img.height());
95        let min_dim = width.min(height);
96        let x_offset = (width - min_dim) / 2;
97        let y_offset = (height - min_dim) / 2;
98
99        let square_img = img.crop_imm(x_offset, y_offset, min_dim, min_dim);
100        square_img.resize_exact(s, s, image::imageops::FilterType::Lanczos3)
101    } else {
102        img
103    };
104
105    let mut buffer = Cursor::new(Vec::new());
106    final_img
107        .write_to(&mut buffer, ImageOutputFormat::Png)
108        .map_err(|e| format!("Failed to write PNG: {}", e))?;
109
110    let png_data = buffer.into_inner();
111    let width = final_img.width();
112    let height = final_img.height();
113
114    let b64 = general_purpose::STANDARD.encode(&png_data);
115
116    let svg = format!(
117        r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
118  <image width="{}" height="{}" xlink:href="data:image/png;base64,{}" />
119</svg>"#,
120        width, height, width, height, b64
121    );
122
123    Ok(svg.into_bytes())
124}
125
126#[wasm_bindgen]
127pub fn imgsvg(input: &[u8], size: Option<u32>) -> Result<Vec<u8>, JsValue> {
128    imgsvg_core(input, size).map_err(|e| JsValue::from_str(&e))
129}
130
131pub fn imgpng_core(input: &[u8], size: Option<u32>) -> Result<Vec<u8>, String> {
132    let img = image::load_from_memory(input).map_err(|e| format!("Failed to load image: {}", e))?;
133
134    let final_img = if let Some(s) = size {
135        // Make square by cropping from center
136        let (width, height) = (img.width(), img.height());
137        let min_dim = width.min(height);
138        let x_offset = (width - min_dim) / 2;
139        let y_offset = (height - min_dim) / 2;
140
141        let square_img = img.crop_imm(x_offset, y_offset, min_dim, min_dim);
142        square_img.resize_exact(s, s, image::imageops::FilterType::Lanczos3)
143    } else {
144        img
145    };
146
147    let mut buffer = Cursor::new(Vec::new());
148    final_img
149        .write_to(&mut buffer, ImageOutputFormat::Png)
150        .map_err(|e| format!("Failed to write PNG: {}", e))?;
151
152    Ok(buffer.into_inner())
153}
154
155#[wasm_bindgen]
156pub fn imgpng(input: &[u8], size: Option<u32>) -> Result<Vec<u8>, JsValue> {
157    imgpng_core(input, size).map_err(|e| JsValue::from_str(&e))
158}