1use std::io::Cursor;
8
9use bnto_core::errors::BntoError;
10use image::DynamicImage;
11use image::codecs::jpeg::JpegEncoder;
12use image::codecs::png::{CompressionType, FilterType, PngEncoder};
13
14use crate::format::ImageFormat;
15
16pub fn encode_image(
24 img: &DynamicImage,
25 format: ImageFormat,
26 quality: u8,
27) -> Result<Vec<u8>, BntoError> {
28 match format {
29 ImageFormat::Jpeg => encode_jpeg(img, quality),
30 ImageFormat::Png => encode_png(img, quality),
31 ImageFormat::WebP => encode_webp(img),
32 }
33}
34
35fn encode_jpeg(img: &DynamicImage, quality: u8) -> Result<Vec<u8>, BntoError> {
36 let mut output = Vec::new();
37 let encoder = JpegEncoder::new_with_quality(&mut output, quality);
38 img.write_with_encoder(encoder)
39 .map_err(|e| BntoError::ProcessingFailed(format!("Failed to encode JPEG: {e}")))?;
40 Ok(output)
41}
42
43fn encode_png(img: &DynamicImage, quality: u8) -> Result<Vec<u8>, BntoError> {
44 let compression = quality_to_png_compression(quality);
45 let mut output = Vec::new();
46 let encoder = PngEncoder::new_with_quality(&mut output, compression, FilterType::Adaptive);
47 img.write_with_encoder(encoder)
48 .map_err(|e| BntoError::ProcessingFailed(format!("Failed to encode PNG: {e}")))?;
49 Ok(output)
50}
51
52fn encode_webp(img: &DynamicImage) -> Result<Vec<u8>, BntoError> {
53 let mut output = Vec::new();
54 let mut cursor_out = Cursor::new(&mut output);
55 img.write_to(&mut cursor_out, image::ImageFormat::WebP)
56 .map_err(|e| BntoError::ProcessingFailed(format!("Failed to encode WebP: {e}")))?;
57 Ok(output)
58}
59
60fn quality_to_png_compression(quality: u8) -> CompressionType {
63 if quality < 33 {
64 CompressionType::Fast
65 } else if quality < 66 {
66 CompressionType::Default
67 } else {
68 CompressionType::Best
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use image::{DynamicImage, Rgb, RgbImage};
76
77 fn create_test_image(width: u32, height: u32) -> DynamicImage {
78 let mut img = RgbImage::new(width, height);
79 for y in 0..height {
80 for x in 0..width {
81 img.put_pixel(x, y, Rgb([(x * 3) as u8, (y * 5) as u8, 128]));
82 }
83 }
84 DynamicImage::ImageRgb8(img)
85 }
86
87 #[test]
88 fn test_encode_jpeg_produces_valid_output() {
89 let img = create_test_image(50, 50);
90 let result = encode_image(&img, ImageFormat::Jpeg, 80).unwrap();
91 assert_eq!(result[0], 0xFF);
92 assert_eq!(result[1], 0xD8);
93 }
94
95 #[test]
96 fn test_encode_png_produces_valid_output() {
97 let img = create_test_image(50, 50);
98 let result = encode_image(&img, ImageFormat::Png, 80).unwrap();
99 assert_eq!(&result[..4], &[0x89, 0x50, 0x4E, 0x47]);
100 }
101
102 #[test]
103 fn test_encode_webp_produces_valid_output() {
104 let img = create_test_image(50, 50);
105 let result = encode_image(&img, ImageFormat::WebP, 80).unwrap();
106 assert_eq!(&result[..4], b"RIFF");
107 assert_eq!(&result[8..12], b"WEBP");
108 }
109
110 #[test]
111 fn test_jpeg_quality_affects_size() {
112 let img = create_test_image(100, 100);
113 let low = encode_image(&img, ImageFormat::Jpeg, 20).unwrap();
114 let high = encode_image(&img, ImageFormat::Jpeg, 90).unwrap();
115 assert!(
116 low.len() < high.len(),
117 "Low quality ({}) should be smaller than high quality ({})",
118 low.len(),
119 high.len()
120 );
121 }
122
123 #[test]
124 fn test_quality_to_png_compression_mapping() {
125 assert!(matches!(
126 quality_to_png_compression(1),
127 CompressionType::Fast
128 ));
129 assert!(matches!(
130 quality_to_png_compression(32),
131 CompressionType::Fast
132 ));
133 assert!(matches!(
134 quality_to_png_compression(33),
135 CompressionType::Default
136 ));
137 assert!(matches!(
138 quality_to_png_compression(65),
139 CompressionType::Default
140 ));
141 assert!(matches!(
142 quality_to_png_compression(66),
143 CompressionType::Best
144 ));
145 assert!(matches!(
146 quality_to_png_compression(100),
147 CompressionType::Best
148 ));
149 }
150}