Skip to main content

icon_to_image/
encoder.rs

1//! Image encoding utilities for PNG and WebP output.
2
3use crate::error::{IconFontError, Result};
4use image::ImageEncoder;
5use std::io::Cursor;
6use std::path::Path;
7
8/// Image output format.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum ImageFormat {
11    #[default]
12    Png,
13    WebP,
14}
15
16impl ImageFormat {
17    pub const fn extension(&self) -> &'static str {
18        match self {
19            ImageFormat::Png => "png",
20            ImageFormat::WebP => "webp",
21        }
22    }
23
24    pub const fn mime_type(&self) -> &'static str {
25        match self {
26            ImageFormat::Png => "image/png",
27            ImageFormat::WebP => "image/webp",
28        }
29    }
30}
31
32/// PNG compression presets for balancing encode speed and output size.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum PngCompression {
35    Fast,
36    Default,
37    #[default]
38    Best,
39}
40
41impl PngCompression {
42    #[inline]
43    const fn settings(
44        self,
45    ) -> (
46        image::codecs::png::CompressionType,
47        image::codecs::png::FilterType,
48    ) {
49        match self {
50            PngCompression::Fast => (
51                image::codecs::png::CompressionType::Fast,
52                image::codecs::png::FilterType::NoFilter,
53            ),
54            PngCompression::Default => (
55                image::codecs::png::CompressionType::Default,
56                image::codecs::png::FilterType::Adaptive,
57            ),
58            PngCompression::Best => (
59                image::codecs::png::CompressionType::Best,
60                image::codecs::png::FilterType::Adaptive,
61            ),
62        }
63    }
64}
65
66/// Encode RGBA pixels to PNG with best compression.
67pub fn encode_png(pixels: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
68    encode_png_with_compression(pixels, width, height, PngCompression::Best)
69}
70
71/// Encode RGBA pixels to PNG with a configurable compression preset.
72pub fn encode_png_with_compression(
73    pixels: &[u8],
74    width: u32,
75    height: u32,
76    compression: PngCompression,
77) -> Result<Vec<u8>> {
78    let mut output = Cursor::new(Vec::new());
79    let (compression_type, filter_type) = compression.settings();
80
81    let encoder = image::codecs::png::PngEncoder::new_with_quality(
82        &mut output,
83        compression_type,
84        filter_type,
85    );
86
87    encoder
88        .write_image(pixels, width, height, image::ExtendedColorType::Rgba8)
89        .map_err(|e| IconFontError::ImageEncodingError(format!("PNG encoding failed: {}", e)))?;
90
91    Ok(output.into_inner())
92}
93
94/// Encode RGBA pixels to lossless WebP.
95pub fn encode_webp(pixels: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
96    let mut output = Cursor::new(Vec::new());
97    let encoder = image::codecs::webp::WebPEncoder::new_lossless(&mut output);
98
99    encoder
100        .write_image(pixels, width, height, image::ExtendedColorType::Rgba8)
101        .map_err(|e| IconFontError::ImageEncodingError(format!("WebP encoding failed: {}", e)))?;
102
103    Ok(output.into_inner())
104}
105
106/// Encode pixels to the specified format.
107pub fn encode(pixels: &[u8], width: u32, height: u32, format: ImageFormat) -> Result<Vec<u8>> {
108    match format {
109        ImageFormat::Png => encode_png(pixels, width, height),
110        ImageFormat::WebP => encode_webp(pixels, width, height),
111    }
112}
113
114/// Save pixels to a file (format determined by extension, defaults to PNG).
115pub fn save_to_file<P: AsRef<Path>>(pixels: &[u8], width: u32, height: u32, path: P) -> Result<()> {
116    let path = path.as_ref();
117    let format = match path.extension().and_then(|e| e.to_str()) {
118        Some("png") => ImageFormat::Png,
119        Some("webp") => ImageFormat::WebP,
120        Some(ext) => {
121            return Err(IconFontError::ImageEncodingError(format!(
122                "Unsupported format: {}",
123                ext
124            )))
125        }
126        None => ImageFormat::Png,
127    };
128
129    let data = encode(pixels, width, height, format)?;
130    std::fs::write(path, data)?;
131    Ok(())
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_format_extension() {
140        assert_eq!(ImageFormat::Png.extension(), "png");
141        assert_eq!(ImageFormat::WebP.extension(), "webp");
142    }
143
144    #[test]
145    fn test_format_mime_type() {
146        assert_eq!(ImageFormat::Png.mime_type(), "image/png");
147        assert_eq!(ImageFormat::WebP.mime_type(), "image/webp");
148    }
149
150    #[test]
151    fn test_png_compression_settings() {
152        let (fast_c, fast_f) = PngCompression::Fast.settings();
153        let (default_c, default_f) = PngCompression::Default.settings();
154        let (best_c, best_f) = PngCompression::Best.settings();
155
156        assert_eq!(fast_c, image::codecs::png::CompressionType::Fast);
157        assert_eq!(fast_f, image::codecs::png::FilterType::NoFilter);
158        assert_eq!(default_c, image::codecs::png::CompressionType::Default);
159        assert_eq!(default_f, image::codecs::png::FilterType::Adaptive);
160        assert_eq!(best_c, image::codecs::png::CompressionType::Best);
161        assert_eq!(best_f, image::codecs::png::FilterType::Adaptive);
162    }
163
164    #[test]
165    fn test_png_compression_default_is_best() {
166        assert_eq!(PngCompression::default(), PngCompression::Best);
167    }
168
169    #[test]
170    fn test_encode_small_image() {
171        // Create a 2x2 red image
172        let pixels = vec![
173            255, 0, 0, 255, // red
174            255, 0, 0, 255, // red
175            255, 0, 0, 255, // red
176            255, 0, 0, 255, // red
177        ];
178
179        let png_data = encode_png(&pixels, 2, 2).unwrap();
180        assert!(!png_data.is_empty());
181        // PNG magic bytes
182        assert_eq!(&png_data[0..4], &[0x89, 0x50, 0x4E, 0x47]);
183
184        let webp_data = encode_webp(&pixels, 2, 2).unwrap();
185        assert!(!webp_data.is_empty());
186        // WebP magic bytes (RIFF....WEBP)
187        assert_eq!(&webp_data[0..4], b"RIFF");
188    }
189}