webp_screenshot_rust/encoder/
webp.rs

1//! WebP encoder implementation using libwebp
2
3use crate::{
4    encoder::{EncoderStats, ImageEncoder},
5    error::{EncodingError, EncodingResult},
6    types::{PixelFormat, RawImage, WebPConfig},
7};
8
9use std::time::Instant;
10
11/// Re-export encoder options
12pub use crate::types::WebPConfig as EncoderOptions;
13
14/// WebP encoder using the webp crate
15pub struct WebPEncoder {
16    stats: EncoderStats,
17}
18
19impl Default for WebPEncoder {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl WebPEncoder {
26    /// Create a new WebP encoder
27    pub fn new() -> Self {
28        Self {
29            stats: EncoderStats::default(),
30        }
31    }
32
33    /// Get encoder statistics
34    pub fn stats(&self) -> &EncoderStats {
35        &self.stats
36    }
37
38    /// Encode an image to WebP format
39    pub fn encode(&mut self, image: &RawImage, config: &WebPConfig) -> EncodingResult<Vec<u8>> {
40        let start_time = Instant::now();
41
42        // Validate configuration
43        config.validate()
44            .map_err(|e| EncodingError::InvalidConfiguration(e))?;
45
46        // Validate image dimensions
47        if image.width == 0 || image.height == 0 {
48            return Err(EncodingError::InvalidDimensions {
49                width: image.width,
50                height: image.height,
51            });
52        }
53
54        // Encode based on pixel format
55        let result = self.encode_with_format(image, config)?;
56
57        // Update statistics
58        let encoding_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
59        self.stats.update(image.size(), result.len(), encoding_time_ms);
60
61        Ok(result)
62    }
63
64    /// Encode with specific pixel format handling
65    fn encode_with_format(&self, image: &RawImage, config: &WebPConfig) -> EncodingResult<Vec<u8>> {
66        // Convert to appropriate format and encode
67        let encoded = match image.format {
68            PixelFormat::RGBA8 => {
69                let encoder = webp::Encoder::from_rgba(&image.data, image.width, image.height);
70                if config.lossless {
71                    encoder.encode_lossless()
72                } else {
73                    encoder.encode(config.quality as f32)
74                }
75            }
76            PixelFormat::RGB8 => {
77                let encoder = webp::Encoder::from_rgb(&image.data, image.width, image.height);
78                if config.lossless {
79                    encoder.encode_lossless()
80                } else {
81                    encoder.encode(config.quality as f32)
82                }
83            }
84            PixelFormat::BGRA8 => {
85                // Convert BGRA to RGBA first
86                let mut rgba_data = image.data.clone();
87                self.convert_bgra_to_rgba_inplace(&mut rgba_data);
88                let encoder = webp::Encoder::from_rgba(&rgba_data, image.width, image.height);
89                if config.lossless {
90                    encoder.encode_lossless()
91                } else {
92                    encoder.encode(config.quality as f32)
93                }
94            }
95            PixelFormat::BGR8 => {
96                // Convert BGR to RGB first
97                let mut rgb_data = image.data.clone();
98                self.convert_bgr_to_rgb_inplace(&mut rgb_data);
99                let encoder = webp::Encoder::from_rgb(&rgb_data, image.width, image.height);
100                if config.lossless {
101                    encoder.encode_lossless()
102                } else {
103                    encoder.encode(config.quality as f32)
104                }
105            }
106            _ => {
107                return Err(EncodingError::UnsupportedFormat(
108                    format!("Unsupported pixel format: {:?}", image.format)
109                ));
110            }
111        };
112
113        Ok(encoded.to_vec())
114    }
115
116    /// Convert BGRA to RGBA in-place
117    fn convert_bgra_to_rgba_inplace(&self, data: &mut [u8]) {
118        for chunk in data.chunks_exact_mut(4) {
119            chunk.swap(0, 2); // Swap B and R
120        }
121    }
122
123    /// Convert BGR to RGB in-place
124    fn convert_bgr_to_rgb_inplace(&self, data: &mut [u8]) {
125        for chunk in data.chunks_exact_mut(3) {
126            chunk.swap(0, 2); // Swap B and R
127        }
128    }
129
130    /// Check if the encoder supports a specific format
131    pub fn supports_format(&self, format: PixelFormat) -> bool {
132        matches!(
133            format,
134            PixelFormat::RGBA8 | PixelFormat::RGB8 | PixelFormat::BGRA8 | PixelFormat::BGR8
135        )
136    }
137
138    /// Get encoder capabilities
139    pub fn capabilities(&self) -> Vec<&'static str> {
140        vec![
141            "WebP encoding",
142            "Lossy compression",
143            "Lossless compression",
144            "RGBA/RGB support",
145            "BGRA/BGR conversion",
146            "Quality control",
147        ]
148    }
149}
150
151impl ImageEncoder for WebPEncoder {
152    fn encode(&self, image: &RawImage, config: &WebPConfig) -> EncodingResult<Vec<u8>> {
153        // Use the internal encode_with_format method
154        self.encode_with_format(image, config)
155    }
156
157    fn name(&self) -> &str {
158        "WebPEncoder"
159    }
160
161    fn supports_format(&self, format: PixelFormat) -> bool {
162        self.supports_format(format)
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_encoder_creation() {
172        let encoder = WebPEncoder::new();
173        assert_eq!(encoder.stats.images_encoded, 0);
174    }
175
176    #[test]
177    fn test_format_support() {
178        let encoder = WebPEncoder::new();
179        assert!(encoder.supports_format(PixelFormat::RGBA8));
180        assert!(encoder.supports_format(PixelFormat::RGB8));
181        assert!(encoder.supports_format(PixelFormat::BGRA8));
182        assert!(encoder.supports_format(PixelFormat::BGR8));
183    }
184
185    #[test]
186    fn test_config_validation() {
187        let mut config = WebPConfig::default();
188        assert!(config.validate().is_ok());
189
190        config.quality = 101;
191        assert!(config.validate().is_err());
192    }
193
194    #[test]
195    fn test_encode_rgba() {
196        let encoder = WebPEncoder::new();
197        let test_image = RawImage {
198            data: vec![255u8; 100 * 100 * 4],
199            width: 100,
200            height: 100,
201            format: PixelFormat::RGBA8,
202            stride: 100 * 4,
203        };
204
205        let config = WebPConfig::default();
206        let result = encoder.encode(&test_image, &config);
207        assert!(result.is_ok());
208
209        let webp_data = result.unwrap();
210        assert!(!webp_data.is_empty());
211    }
212}