imageoptimize/
images.rs

1use avif_decode::Decoder;
2use image::codecs::avif;
3use image::codecs::gif;
4use image::codecs::webp;
5use image::{AnimationDecoder, DynamicImage, ImageEncoder, ImageFormat, RgbaImage};
6use lodepng::Bitmap;
7use rgb::{ComponentBytes, RGB8, RGBA8};
8use snafu::{ResultExt, Snafu};
9use std::{
10    ffi::OsStr,
11    io::{BufRead, Seek},
12};
13
14#[derive(Debug, Snafu)]
15pub enum ImageError {
16    #[snafu(display("Handle image fail, category:{category}, message:{source}"))]
17    Image {
18        category: String,
19        source: image::ImageError,
20    },
21    #[snafu(display("Handle image fail, category:{category}, message:{source}"))]
22    ImageQuant {
23        category: String,
24        source: imagequant::Error,
25    },
26    #[snafu(display("Handle image fail, category:{category}, message:{source}"))]
27    AvifDecode {
28        category: String,
29        source: avif_decode::Error,
30    },
31    #[snafu(display("Handle image fail, category:{category}, message:{source}"))]
32    LodePNG {
33        category: String,
34        source: lodepng::Error,
35    },
36    #[snafu(display("Handle image fail, category:mozjpeg, message:unknown"))]
37    Mozjpeg {},
38    #[snafu(display("Io fail, {source}"))]
39    Io { source: std::io::Error },
40    #[snafu(display("Handle image fail"))]
41    Unknown,
42}
43
44type Result<T, E = ImageError> = std::result::Result<T, E>;
45
46pub struct ImageInfo {
47    // rgba像素
48    pub buffer: Vec<RGBA8>,
49    /// Width in pixels
50    pub width: usize,
51    /// Height in pixels
52    pub height: usize,
53}
54
55impl From<Bitmap<RGBA8>> for ImageInfo {
56    fn from(info: Bitmap<RGBA8>) -> Self {
57        ImageInfo {
58            buffer: info.buffer,
59            width: info.width,
60            height: info.height,
61        }
62    }
63}
64
65impl From<RgbaImage> for ImageInfo {
66    fn from(img: RgbaImage) -> Self {
67        let width = img.width() as usize;
68        let height = img.height() as usize;
69        let mut buffer = Vec::with_capacity(width * height);
70
71        for ele in img.chunks(4) {
72            buffer.push(RGBA8 {
73                r: ele[0],
74                g: ele[1],
75                b: ele[2],
76                a: ele[3],
77            })
78        }
79
80        ImageInfo {
81            buffer,
82            width,
83            height,
84        }
85    }
86}
87
88/// Decode data from avif format, it supports rgb8,
89/// rgba8, rgb16 and rgba16.
90pub fn avif_decode(data: &[u8]) -> Result<DynamicImage> {
91    let avif_result = Decoder::from_avif(data)
92        .context(AvifDecodeSnafu {
93            category: "decode".to_string(),
94        })?
95        .to_image()
96        .context(AvifDecodeSnafu {
97            category: "decode".to_string(),
98        })?;
99    match avif_result {
100        avif_decode::Image::Rgb8(img) => {
101            let width = img.width();
102            let height = img.height();
103            let mut buf = Vec::with_capacity(width * height * 3);
104            for item in img.buf() {
105                buf.push(item.r);
106                buf.push(item.g);
107                buf.push(item.b);
108            }
109            let rgb_image = image::RgbImage::from_raw(width as u32, height as u32, buf)
110                .ok_or(ImageError::Unknown)?;
111            Ok(DynamicImage::ImageRgb8(rgb_image))
112        }
113        avif_decode::Image::Rgba8(img) => {
114            let width = img.width();
115            let height = img.height();
116            let mut buf = Vec::with_capacity(width * height * 4);
117            for item in img.buf() {
118                buf.push(item.r);
119                buf.push(item.g);
120                buf.push(item.b);
121                buf.push(item.a);
122            }
123            let rgba_image = image::RgbaImage::from_raw(width as u32, height as u32, buf)
124                .ok_or(ImageError::Unknown)?;
125            Ok(DynamicImage::ImageRgba8(rgba_image))
126        }
127        avif_decode::Image::Rgba16(img) => {
128            let width = img.width();
129            let height = img.height();
130            let mut buf = Vec::with_capacity(width * height * 4);
131            for item in img.buf() {
132                buf.push((item.r / 257) as u8);
133                buf.push((item.g / 257) as u8);
134                buf.push((item.b / 257) as u8);
135                buf.push((item.a / 257) as u8);
136            }
137            let rgba_image = image::RgbaImage::from_raw(width as u32, height as u32, buf)
138                .ok_or(ImageError::Unknown)?;
139            Ok(DynamicImage::ImageRgba8(rgba_image))
140        }
141        avif_decode::Image::Rgb16(img) => {
142            let width = img.width();
143            let height = img.height();
144            let mut buf = Vec::with_capacity(width * height * 3);
145            for item in img.buf() {
146                buf.push((item.r / 257) as u8);
147                buf.push((item.g / 257) as u8);
148                buf.push((item.b / 257) as u8);
149            }
150            let rgb_image = image::RgbImage::from_raw(width as u32, height as u32, buf)
151                .ok_or(ImageError::Unknown)?;
152            Ok(DynamicImage::ImageRgb8(rgb_image))
153        }
154        _ => Err(ImageError::Unknown),
155    }
156}
157
158pub fn load<R: BufRead + Seek>(r: R, ext: &str) -> Result<ImageInfo> {
159    let format = ImageFormat::from_extension(OsStr::new(ext)).unwrap_or(ImageFormat::Jpeg);
160    let result = image::load(r, format).context(ImageSnafu { category: "load" })?;
161    let img = result.to_rgba8();
162    Ok(img.into())
163}
164
165pub fn to_gif<R>(r: R, speed: u8) -> Result<Vec<u8>>
166where
167    R: std::io::BufRead,
168    R: std::io::Seek,
169{
170    let decoder = gif::GifDecoder::new(r).context(ImageSnafu {
171        category: "gif_decode",
172    })?;
173    let frames = decoder.into_frames();
174
175    let mut w = Vec::new();
176
177    {
178        let mut encoder = gif::GifEncoder::new_with_speed(&mut w, speed as i32);
179        encoder
180            .set_repeat(gif::Repeat::Infinite)
181            .context(ImageSnafu {
182                category: "gif_set_repeat",
183            })?;
184        encoder
185            .try_encode_frames(frames.into_iter())
186            .context(ImageSnafu {
187                category: "git_encode",
188            })?;
189    }
190
191    Ok(w)
192}
193
194impl ImageInfo {
195    // 转换获取rgb颜色
196    fn get_rgb8(&self) -> Vec<RGB8> {
197        let mut output_data: Vec<RGB8> = Vec::with_capacity(self.width * self.height);
198
199        for ele in &self.buffer {
200            output_data.push(ele.rgb())
201        }
202
203        output_data
204    }
205    /// Optimize image to png, the quality is min 0, max 100, which means best effort,
206    /// and never aborts the process.
207    pub fn to_png(&self, quality: u8) -> Result<Vec<u8>> {
208        let mut liq = imagequant::new();
209        liq.set_quality(0, quality).context(ImageQuantSnafu {
210            category: "png_set_quality",
211        })?;
212
213        let mut img = liq
214            .new_image(self.buffer.as_ref(), self.width, self.height, 0.0)
215            .context(ImageQuantSnafu {
216                category: "png_new_image",
217            })?;
218
219        let mut res = liq.quantize(&mut img).context(ImageQuantSnafu {
220            category: "png_quantize",
221        })?;
222
223        res.set_dithering_level(1.0).context(ImageQuantSnafu {
224            category: "png_set_level",
225        })?;
226
227        let (palette, pixels) = res.remapped(&mut img).context(ImageQuantSnafu {
228            category: "png_remapped",
229        })?;
230        let mut enc = lodepng::Encoder::new();
231        enc.set_palette(&palette).context(LodePNGSnafu {
232            category: "png_encoder",
233        })?;
234
235        let buf = enc
236            .encode(&pixels, self.width, self.height)
237            .context(LodePNGSnafu {
238                category: "png_encode",
239            })?;
240
241        Ok(buf)
242    }
243    /// Optimize image to lossless webp.
244    pub fn to_webp(&self) -> Result<Vec<u8>> {
245        let mut w = Vec::new();
246
247        let img = webp::WebPEncoder::new_lossless(&mut w);
248
249        img.encode(
250            self.buffer.as_bytes(),
251            self.width as u32,
252            self.height as u32,
253            image::ColorType::Rgba8.into(),
254        )
255        .context(ImageSnafu {
256            category: "webp_encode",
257        })?;
258
259        Ok(w)
260    }
261    /// Optimize image to avif.
262    /// `speed` accepts a value in the range 0-10, where 0 is the slowest and 10 is the fastest.
263    /// `quality` accepts a value in the range 0-100, where 0 is the worst and 100 is the best.
264    pub fn to_avif(&self, quality: u8, speed: u8) -> Result<Vec<u8>> {
265        let mut w = Vec::new();
266        let mut sp = speed;
267        if sp == 0 {
268            sp = 3;
269        }
270
271        let img = avif::AvifEncoder::new_with_speed_quality(&mut w, sp, quality);
272        img.write_image(
273            self.buffer.as_bytes(),
274            self.width as u32,
275            self.height as u32,
276            image::ColorType::Rgba8.into(),
277        )
278        .context(ImageSnafu {
279            category: "avif_encode",
280        })?;
281
282        Ok(w)
283    }
284    /// Optimize image to jpeg, the quality 60-80 are recommended.
285    pub fn to_mozjpeg(&self, quality: u8) -> Result<Vec<u8>> {
286        let mut comp = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_RGB);
287        comp.set_size(self.width, self.height);
288        comp.set_quality(quality as f32);
289        let mut comp = comp.start_compress(Vec::new()).context(IoSnafu {})?;
290        comp.write_scanlines(self.get_rgb8().as_bytes())
291            .context(IoSnafu {})?;
292        let data = comp.finish().context(IoSnafu {})?;
293        Ok(data)
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::{load, ImageInfo};
300    use pretty_assertions::assert_eq;
301
302    use std::io::Cursor;
303    fn load_image() -> ImageInfo {
304        let data = include_bytes!("../assets/rust-logo.png");
305        load(Cursor::new(data), "png").unwrap()
306    }
307
308    #[test]
309    fn test_load_image() {
310        let img = load_image();
311        assert_eq!(img.height, 144);
312        assert_eq!(img.width, 144);
313    }
314    #[test]
315    fn test_to_png() {
316        let img = load_image();
317        let result = img.to_png(90).unwrap();
318        // 直接判断长度可能导致版本更新则需要重新修改测试
319        assert_eq!(result.len(), 1665);
320    }
321    #[test]
322    fn test_to_webp() {
323        let img = load_image();
324        let result = img.to_webp().unwrap();
325        assert_eq!(result.len(), 2764);
326    }
327    #[test]
328    fn test_to_jpeg() {
329        let img = load_image();
330        let result = img.to_mozjpeg(90).unwrap();
331        assert_eq!(result.len(), 392);
332    }
333    #[test]
334    fn test_to_avif() {
335        let img = load_image();
336        let result = img.to_avif(90, 3).unwrap();
337        assert_eq!(result.len(), 2345);
338    }
339}