1use crate::error::{IconFontError, Result};
4use image::ImageEncoder;
5use std::io::Cursor;
6use std::path::Path;
7
8#[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#[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
66pub fn encode_png(pixels: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
68 encode_png_with_compression(pixels, width, height, PngCompression::Best)
69}
70
71pub 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
94pub 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
106pub 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
114pub 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 let pixels = vec![
173 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, ];
178
179 let png_data = encode_png(&pixels, 2, 2).unwrap();
180 assert!(!png_data.is_empty());
181 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 assert_eq!(&webp_data[0..4], b"RIFF");
188 }
189}