webp_screenshot_rust/
types.rs

1//! Core types and structures for screenshot capture and WebP encoding
2
3use std::fmt;
4use std::time::{Duration, SystemTime};
5
6/// Information about a display/monitor
7#[derive(Debug, Clone, PartialEq)]
8pub struct DisplayInfo {
9    /// Display index (0-based)
10    pub index: usize,
11    /// Display name or identifier
12    pub name: String,
13    /// Width in pixels
14    pub width: u32,
15    /// Height in pixels
16    pub height: u32,
17    /// X offset from primary display
18    pub x: i32,
19    /// Y offset from primary display
20    pub y: i32,
21    /// Scale factor (for HiDPI displays)
22    pub scale_factor: f32,
23    /// Whether this is the primary display
24    pub is_primary: bool,
25    /// Refresh rate in Hz
26    pub refresh_rate: u32,
27    /// Color depth in bits
28    pub color_depth: u8,
29}
30
31impl Default for DisplayInfo {
32    fn default() -> Self {
33        Self {
34            index: 0,
35            name: "Primary Display".to_string(),
36            width: 1920,
37            height: 1080,
38            x: 0,
39            y: 0,
40            scale_factor: 1.0,
41            is_primary: true,
42            refresh_rate: 60,
43            color_depth: 32,
44        }
45    }
46}
47
48impl DisplayInfo {
49    /// Get the total pixel count
50    pub fn pixel_count(&self) -> u32 {
51        self.width * self.height
52    }
53
54    /// Get the display bounds as a rectangle
55    pub fn bounds(&self) -> Rectangle {
56        Rectangle {
57            x: self.x,
58            y: self.y,
59            width: self.width,
60            height: self.height,
61        }
62    }
63}
64
65/// Pixel format for raw image data
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum PixelFormat {
68    /// Red, Green, Blue, Alpha (8 bits per channel)
69    RGBA8,
70    /// Blue, Green, Red, Alpha (8 bits per channel)
71    BGRA8,
72    /// Red, Green, Blue (8 bits per channel)
73    RGB8,
74    /// Blue, Green, Red (8 bits per channel)
75    BGR8,
76    /// Grayscale (8 bits)
77    Gray8,
78    /// Grayscale with alpha (8 bits per channel)
79    GrayA8,
80}
81
82impl PixelFormat {
83    /// Get the number of bytes per pixel
84    pub fn bytes_per_pixel(&self) -> usize {
85        match self {
86            PixelFormat::RGBA8 | PixelFormat::BGRA8 => 4,
87            PixelFormat::RGB8 | PixelFormat::BGR8 => 3,
88            PixelFormat::GrayA8 => 2,
89            PixelFormat::Gray8 => 1,
90        }
91    }
92
93    /// Check if the format has an alpha channel
94    pub fn has_alpha(&self) -> bool {
95        matches!(self, PixelFormat::RGBA8 | PixelFormat::BGRA8 | PixelFormat::GrayA8)
96    }
97
98    /// Get the number of color channels
99    pub fn channel_count(&self) -> usize {
100        match self {
101            PixelFormat::RGBA8 | PixelFormat::BGRA8 => 4,
102            PixelFormat::RGB8 | PixelFormat::BGR8 => 3,
103            PixelFormat::GrayA8 => 2,
104            PixelFormat::Gray8 => 1,
105        }
106    }
107}
108
109impl fmt::Display for PixelFormat {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            PixelFormat::RGBA8 => write!(f, "RGBA8"),
113            PixelFormat::BGRA8 => write!(f, "BGRA8"),
114            PixelFormat::RGB8 => write!(f, "RGB8"),
115            PixelFormat::BGR8 => write!(f, "BGR8"),
116            PixelFormat::Gray8 => write!(f, "Gray8"),
117            PixelFormat::GrayA8 => write!(f, "GrayA8"),
118        }
119    }
120}
121
122/// Raw image data container
123#[derive(Debug, Clone)]
124pub struct RawImage {
125    /// Pixel data
126    pub data: Vec<u8>,
127    /// Image width in pixels
128    pub width: u32,
129    /// Image height in pixels
130    pub height: u32,
131    /// Pixel format
132    pub format: PixelFormat,
133    /// Stride (bytes per row, may include padding)
134    pub stride: usize,
135}
136
137impl RawImage {
138    /// Create a new RawImage
139    pub fn new(data: Vec<u8>, width: u32, height: u32, format: PixelFormat) -> Self {
140        let stride = (width as usize) * format.bytes_per_pixel();
141        Self {
142            data,
143            width,
144            height,
145            format,
146            stride,
147        }
148    }
149
150    /// Create a new RawImage with custom stride
151    pub fn with_stride(
152        data: Vec<u8>,
153        width: u32,
154        height: u32,
155        format: PixelFormat,
156        stride: usize,
157    ) -> Self {
158        Self {
159            data,
160            width,
161            height,
162            format,
163            stride,
164        }
165    }
166
167    /// Get the total size in bytes
168    pub fn size(&self) -> usize {
169        self.data.len()
170    }
171
172    /// Get the pixel count
173    pub fn pixel_count(&self) -> usize {
174        (self.width * self.height) as usize
175    }
176
177    /// Check if the image data is valid
178    pub fn is_valid(&self) -> bool {
179        let expected_size = self.stride * (self.height as usize);
180        self.data.len() >= expected_size
181    }
182
183    /// Get a pixel at the given coordinates
184    pub fn get_pixel(&self, x: u32, y: u32) -> Option<&[u8]> {
185        if x >= self.width || y >= self.height {
186            return None;
187        }
188
189        let offset = (y as usize) * self.stride + (x as usize) * self.format.bytes_per_pixel();
190        let pixel_size = self.format.bytes_per_pixel();
191
192        self.data.get(offset..offset + pixel_size)
193    }
194}
195
196/// WebP encoding configuration
197#[derive(Debug, Clone)]
198pub struct WebPConfig {
199    /// Quality factor (0-100, where 100 is best quality)
200    pub quality: u8,
201    /// Compression method (0-6, where 0 is fastest, 6 is best compression)
202    pub method: u8,
203    /// Enable lossless compression
204    pub lossless: bool,
205    /// Near-lossless encoding quality (0-100, only with lossless=true)
206    pub near_lossless: u8,
207    /// Number of segments (1-4)
208    pub segments: u8,
209    /// Spatial noise shaping strength (0-100)
210    pub sns_strength: u8,
211    /// Filter strength (0-100)
212    pub filter_strength: u8,
213    /// Filter sharpness (0-7)
214    pub filter_sharpness: u8,
215    /// Enable auto-filter
216    pub auto_filter: bool,
217    /// Alpha channel compression (true = compressed, false = uncompressed)
218    pub alpha_compression: bool,
219    /// Alpha filtering level (0-2)
220    pub alpha_filtering: u8,
221    /// Alpha quality (0-100)
222    pub alpha_quality: u8,
223    /// Number of entropy-analysis passes (1-10)
224    pub pass: u8,
225    /// Thread count (0 = auto-detect)
226    pub thread_count: usize,
227    /// Enable low memory mode
228    pub low_memory: bool,
229    /// Preserve RGB values under transparency
230    pub exact: bool,
231}
232
233impl Default for WebPConfig {
234    fn default() -> Self {
235        Self {
236            quality: 80,
237            method: 4,
238            lossless: false,
239            near_lossless: 100,
240            segments: 4,
241            sns_strength: 50,
242            filter_strength: 60,
243            filter_sharpness: 0,
244            auto_filter: false,
245            alpha_compression: true,
246            alpha_filtering: 1,
247            alpha_quality: 100,
248            pass: 1,
249            thread_count: 0,
250            low_memory: false,
251            exact: false,
252        }
253    }
254}
255
256impl WebPConfig {
257    /// Create a high-quality preset
258    pub fn high_quality() -> Self {
259        Self {
260            quality: 95,
261            method: 6,
262            pass: 10,
263            ..Default::default()
264        }
265    }
266
267    /// Create a fast encoding preset
268    pub fn fast() -> Self {
269        Self {
270            quality: 75,
271            method: 0,
272            pass: 1,
273            ..Default::default()
274        }
275    }
276
277    /// Create a lossless encoding preset
278    pub fn lossless() -> Self {
279        Self {
280            lossless: true,
281            quality: 100,
282            method: 6,
283            ..Default::default()
284        }
285    }
286
287    /// Create a balanced preset (good quality/speed tradeoff)
288    pub fn balanced() -> Self {
289        Self {
290            quality: 85,
291            method: 4,
292            pass: 6,
293            ..Default::default()
294        }
295    }
296
297    /// Validate the configuration
298    pub fn validate(&self) -> Result<(), String> {
299        if self.quality > 100 {
300            return Err(format!("Quality must be 0-100, got {}", self.quality));
301        }
302        if self.method > 6 {
303            return Err(format!("Method must be 0-6, got {}", self.method));
304        }
305        if self.segments < 1 || self.segments > 4 {
306            return Err(format!("Segments must be 1-4, got {}", self.segments));
307        }
308        if self.filter_sharpness > 7 {
309            return Err(format!(
310                "Filter sharpness must be 0-7, got {}",
311                self.filter_sharpness
312            ));
313        }
314        if self.alpha_filtering > 2 {
315            return Err(format!(
316                "Alpha filtering must be 0-2, got {}",
317                self.alpha_filtering
318            ));
319        }
320        if self.pass < 1 || self.pass > 10 {
321            return Err(format!("Pass must be 1-10, got {}", self.pass));
322        }
323        Ok(())
324    }
325}
326
327/// Capture configuration
328#[derive(Debug, Clone)]
329pub struct CaptureConfig {
330    /// WebP encoding configuration
331    pub webp_config: WebPConfig,
332    /// Include cursor in capture
333    pub include_cursor: bool,
334    /// Capture region (None for full display)
335    pub region: Option<CaptureRegion>,
336    /// Enable hardware acceleration if available
337    pub use_hardware_acceleration: bool,
338    /// Maximum capture retries
339    pub max_retries: u32,
340    /// Retry delay
341    pub retry_delay: Duration,
342    /// Capture timeout
343    pub timeout: Duration,
344}
345
346impl Default for CaptureConfig {
347    fn default() -> Self {
348        Self {
349            webp_config: WebPConfig::default(),
350            include_cursor: false,
351            region: None,
352            use_hardware_acceleration: true,
353            max_retries: 3,
354            retry_delay: Duration::from_millis(100),
355            timeout: Duration::from_secs(5),
356        }
357    }
358}
359
360/// Capture region specification
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub struct CaptureRegion {
363    pub x: i32,
364    pub y: i32,
365    pub width: u32,
366    pub height: u32,
367}
368
369impl CaptureRegion {
370    pub fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
371        Self { x, y, width, height }
372    }
373
374    pub fn from_rect(rect: Rectangle) -> Self {
375        Self {
376            x: rect.x,
377            y: rect.y,
378            width: rect.width,
379            height: rect.height,
380        }
381    }
382}
383
384/// Rectangle structure
385#[derive(Debug, Clone, Copy, PartialEq, Eq)]
386pub struct Rectangle {
387    pub x: i32,
388    pub y: i32,
389    pub width: u32,
390    pub height: u32,
391}
392
393impl Rectangle {
394    pub fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
395        Self { x, y, width, height }
396    }
397
398    pub fn contains_point(&self, px: i32, py: i32) -> bool {
399        px >= self.x
400            && py >= self.y
401            && px < self.x + self.width as i32
402            && py < self.y + self.height as i32
403    }
404
405    pub fn area(&self) -> u32 {
406        self.width * self.height
407    }
408}
409
410/// Screenshot result with metadata
411#[derive(Debug, Clone)]
412pub struct Screenshot {
413    /// Encoded WebP data
414    pub data: Vec<u8>,
415    /// Image width
416    pub width: u32,
417    /// Image height
418    pub height: u32,
419    /// Display index this was captured from
420    pub display_index: usize,
421    /// Capture metadata
422    pub metadata: CaptureMetadata,
423}
424
425impl Screenshot {
426    /// Get the size of the encoded data
427    pub fn size(&self) -> usize {
428        self.data.len()
429    }
430
431    /// Save to file
432    pub fn save(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
433        use std::fs::File;
434        use std::io::Write;
435
436        let mut file = File::create(path)?;
437        file.write_all(&self.data)?;
438        Ok(())
439    }
440}
441
442/// Capture metadata
443#[derive(Debug, Clone)]
444pub struct CaptureMetadata {
445    /// Timestamp of capture
446    pub timestamp: SystemTime,
447    /// Time taken to capture
448    pub capture_duration: Duration,
449    /// Time taken to encode
450    pub encoding_duration: Duration,
451    /// Original uncompressed size
452    pub original_size: usize,
453    /// Compressed size
454    pub compressed_size: usize,
455    /// Implementation used
456    pub implementation: String,
457}
458
459impl CaptureMetadata {
460    /// Calculate compression ratio (0.0 - 1.0)
461    pub fn compression_ratio(&self) -> f64 {
462        if self.original_size == 0 {
463            0.0
464        } else {
465            self.compressed_size as f64 / self.original_size as f64
466        }
467    }
468
469    /// Get total processing time
470    pub fn total_duration(&self) -> Duration {
471        self.capture_duration + self.encoding_duration
472    }
473
474    /// Get space savings percentage
475    pub fn space_savings_percent(&self) -> f64 {
476        if self.original_size == 0 {
477            0.0
478        } else {
479            (1.0 - self.compression_ratio()) * 100.0
480        }
481    }
482}
483
484/// Performance statistics
485#[derive(Debug, Clone, Default)]
486pub struct PerformanceStats {
487    pub total_captures: u64,
488    pub successful_captures: u64,
489    pub failed_captures: u64,
490    pub total_bytes_captured: u64,
491    pub total_bytes_encoded: u64,
492    pub total_capture_time: Duration,
493    pub total_encoding_time: Duration,
494    pub fastest_capture: Duration,
495    pub slowest_capture: Duration,
496}
497
498impl PerformanceStats {
499    /// Get success rate as percentage
500    pub fn success_rate(&self) -> f64 {
501        if self.total_captures == 0 {
502            0.0
503        } else {
504            (self.successful_captures as f64 / self.total_captures as f64) * 100.0
505        }
506    }
507
508    /// Get average capture time
509    pub fn average_capture_time(&self) -> Duration {
510        if self.successful_captures == 0 {
511            Duration::ZERO
512        } else {
513            self.total_capture_time / self.successful_captures as u32
514        }
515    }
516
517    /// Get average compression ratio
518    pub fn average_compression_ratio(&self) -> f64 {
519        if self.total_bytes_captured == 0 {
520            0.0
521        } else {
522            self.total_bytes_encoded as f64 / self.total_bytes_captured as f64
523        }
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_pixel_format_bytes_per_pixel() {
533        assert_eq!(PixelFormat::RGBA8.bytes_per_pixel(), 4);
534        assert_eq!(PixelFormat::RGB8.bytes_per_pixel(), 3);
535        assert_eq!(PixelFormat::Gray8.bytes_per_pixel(), 1);
536    }
537
538    #[test]
539    fn test_webp_config_validation() {
540        let mut config = WebPConfig::default();
541        assert!(config.validate().is_ok());
542
543        config.quality = 101;
544        assert!(config.validate().is_err());
545    }
546
547    #[test]
548    fn test_raw_image_pixel_access() {
549        let data = vec![255; 1920 * 1080 * 4];
550        let image = RawImage::new(data, 1920, 1080, PixelFormat::RGBA8);
551
552        let pixel = image.get_pixel(0, 0).unwrap();
553        assert_eq!(pixel, &[255, 255, 255, 255]);
554
555        assert!(image.get_pixel(1920, 0).is_none());
556    }
557}