webp_screenshot_rust/
lib.rs

1//! WebP Screenshot Capture Library
2//!
3//! A high-performance, cross-platform library for capturing screenshots
4//! and encoding them as WebP images with minimal overhead.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use webp_screenshot_rust::{WebPScreenshot, CaptureConfig};
10//!
11//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
12//! // Simple capture
13//! let screenshot = WebPScreenshot::new()?;
14//! let result = screenshot.capture_display(0)?;
15//! result.save("screenshot.webp")?;
16//!
17//! // With custom configuration
18//! let config = CaptureConfig {
19//!     include_cursor: true,
20//!     ..Default::default()
21//! };
22//! let screenshot = WebPScreenshot::with_config(config)?;
23//! let result = screenshot.capture_display(0)?;
24//! # Ok(())
25//! # }
26//! ```
27
28#![allow(missing_docs)]
29#![cfg_attr(docsrs, feature(doc_cfg))]
30
31pub mod capture;
32pub mod encoder;
33pub mod error;
34pub mod memory_pool;
35pub mod pipeline;
36pub mod types;
37
38#[cfg(feature = "c-api")]
39pub mod ffi;
40
41// Re-export main types
42pub use capture::{Capturer, ScreenCapture};
43pub use encoder::{WebPEncoder, EncoderOptions};
44pub use error::{CaptureError, CaptureResult, EncodingError, EncodingResult};
45pub use memory_pool::{MemoryPool, PooledBuffer};
46pub use pipeline::{StreamingPipeline, StreamingPipelineBuilder, ZeroCopyOptimizer};
47pub use types::{
48    CaptureConfig, CaptureMetadata, CaptureRegion, DisplayInfo, PerformanceStats, PixelFormat,
49    RawImage, Rectangle, Screenshot, WebPConfig,
50};
51
52use std::sync::Arc;
53use std::time::{Duration, Instant, SystemTime};
54
55/// Main entry point for screenshot capture
56pub struct WebPScreenshot {
57    capturer: Box<dyn ScreenCapture>,
58    encoder: WebPEncoder,
59    memory_pool: Arc<MemoryPool>,
60    config: CaptureConfig,
61    stats: PerformanceStats,
62    zero_copy: Option<ZeroCopyOptimizer>,
63    gpu_encoder: Option<encoder::gpu::GpuWebPEncoder>,
64}
65
66impl WebPScreenshot {
67    /// Create a new WebPScreenshot instance with default configuration
68    pub fn new() -> CaptureResult<Self> {
69        Self::with_config(CaptureConfig::default())
70    }
71
72    /// Create a new instance with custom configuration
73    pub fn with_config(config: CaptureConfig) -> CaptureResult<Self> {
74        let zero_copy = if ZeroCopyOptimizer::is_supported() {
75            Some(ZeroCopyOptimizer::new())
76        } else {
77            None
78        };
79
80        let gpu_encoder = if cfg!(feature = "gpu") {
81            Some(encoder::gpu::GpuWebPEncoder::new())
82        } else {
83            None
84        };
85
86        Ok(Self {
87            capturer: Capturer::new()?,
88            encoder: WebPEncoder::new(),
89            memory_pool: memory_pool::global_pool(),
90            config,
91            stats: PerformanceStats::default(),
92            zero_copy,
93            gpu_encoder,
94        })
95    }
96
97    /// Get information about available displays
98    pub fn get_displays(&self) -> CaptureResult<Vec<DisplayInfo>> {
99        self.capturer.get_displays()
100    }
101
102    /// Capture a screenshot from a specific display
103    pub fn capture_display(&mut self, display_index: usize) -> CaptureResult<Screenshot> {
104        eprintln!("[LIB] ========================================");
105        eprintln!("[LIB] capture_display called with display_index: {}", display_index);
106        eprintln!("[LIB] Config region: {:?}", self.config.region);
107        eprintln!("[LIB] ========================================");
108
109        let start_time = Instant::now();
110        let timestamp = SystemTime::now();
111
112        // Retry logic
113        let mut last_error = None;
114        for attempt in 0..=self.config.max_retries {
115            if attempt > 0 {
116                std::thread::sleep(self.config.retry_delay);
117                log::debug!("Retry attempt {} for display {}", attempt, display_index);
118            }
119
120            match self.capture_display_internal(display_index, timestamp, start_time) {
121                Ok(screenshot) => {
122                    self.stats.successful_captures += 1;
123                    self.stats.total_captures += 1;
124                    return Ok(screenshot);
125                }
126                Err(e) if e.is_recoverable() && attempt < self.config.max_retries => {
127                    last_error = Some(e);
128                    continue;
129                }
130                Err(e) => {
131                    self.stats.failed_captures += 1;
132                    self.stats.total_captures += 1;
133                    return Err(e);
134                }
135            }
136        }
137
138        self.stats.failed_captures += 1;
139        self.stats.total_captures += 1;
140        Err(last_error.unwrap_or_else(|| {
141            CaptureError::CaptureFailed("Max retries exceeded".to_string())
142        }))
143    }
144
145    /// Internal capture implementation
146    fn capture_display_internal(
147        &mut self,
148        display_index: usize,
149        timestamp: SystemTime,
150        start_time: Instant,
151    ) -> CaptureResult<Screenshot> {
152        // Capture raw image
153        let capture_start = Instant::now();
154
155        let raw_image = if let Some(ref zero_copy) = self.zero_copy {
156            // Disable zero-copy when capturing a specific region
157            // Zero-copy is optimized for full-screen captures, not regions
158            if zero_copy.is_enabled() && self.config.region.is_none() {
159                eprintln!("[LIB] Using ZERO-COPY optimization path (full screen)");
160                zero_copy.capture_zero_copy(&*self.capturer, display_index)?
161            } else {
162                if self.config.region.is_some() {
163                    eprintln!("[LIB] Region set - disabling zero-copy, using normal GDI path");
164                } else {
165                    eprintln!("[LIB] Zero-copy available but disabled, using normal path");
166                }
167                self.capture_normal(display_index)?
168            }
169        } else {
170            eprintln!("[LIB] No zero-copy, using normal capture path");
171            self.capture_normal(display_index)?
172        };
173
174        let capture_duration = capture_start.elapsed();
175
176        // Update stats
177        self.stats.total_bytes_captured += raw_image.size() as u64;
178        self.stats.total_capture_time += capture_duration;
179
180        // Encode to WebP
181        let encoding_start = Instant::now();
182
183        let webp_data = if let Some(ref gpu_encoder) = self.gpu_encoder {
184            if gpu_encoder.is_available() && gpu_encoder.is_size_suitable(raw_image.width, raw_image.height) {
185                gpu_encoder.encode(&raw_image, &self.config.webp_config)?
186            } else {
187                self.encoder.encode(&raw_image, &self.config.webp_config)
188                    .map_err(|e| CaptureError::Other(e.into()))?
189            }
190        } else {
191            self.encoder.encode(&raw_image, &self.config.webp_config)
192                .map_err(|e| CaptureError::Other(e.into()))?
193        };
194
195        let encoding_duration = encoding_start.elapsed();
196
197        // Update stats
198        self.stats.total_bytes_encoded += webp_data.len() as u64;
199        self.stats.total_encoding_time += encoding_duration;
200
201        // Update timing records
202        let total_duration = start_time.elapsed();
203        if self.stats.fastest_capture == Duration::ZERO
204            || total_duration < self.stats.fastest_capture
205        {
206            self.stats.fastest_capture = total_duration;
207        }
208        if total_duration > self.stats.slowest_capture {
209            self.stats.slowest_capture = total_duration;
210        }
211
212        // Build metadata
213        let metadata = CaptureMetadata {
214            timestamp,
215            capture_duration,
216            encoding_duration,
217            original_size: raw_image.size(),
218            compressed_size: webp_data.len(),
219            implementation: self.capturer.implementation_name(),
220        };
221
222        Ok(Screenshot {
223            data: webp_data,
224            width: raw_image.width,
225            height: raw_image.height,
226            display_index,
227            metadata,
228        })
229    }
230
231    /// Normal capture without zero-copy
232    fn capture_normal(&self, display_index: usize) -> CaptureResult<RawImage> {
233        if let Some(region) = self.config.region {
234            eprintln!("[LIB] capture_normal: Using region mode - {:?}", region);
235            eprintln!("[LIB] Calling capturer.capture_region()");
236            self.capturer.capture_region(region)
237        } else {
238            eprintln!("[LIB] capture_normal: Using display mode - display {}", display_index);
239            eprintln!("[LIB] Calling capturer.capture_display()");
240            self.capturer.capture_display(display_index)
241        }
242    }
243
244    /// Capture screenshots from all available displays
245    pub fn capture_all_displays(&mut self) -> Vec<CaptureResult<Screenshot>> {
246        match self.get_displays() {
247            Ok(displays) => displays
248                .iter()
249                .enumerate()
250                .map(|(index, _)| self.capture_display(index))
251                .collect(),
252            Err(e) => vec![Err(e)],
253        }
254    }
255
256    /// Capture with a custom encoder configuration
257    pub fn capture_with_config(
258        &mut self,
259        display_index: usize,
260        webp_config: WebPConfig,
261    ) -> CaptureResult<Screenshot> {
262        let original_config = self.config.webp_config.clone();
263        self.config.webp_config = webp_config;
264        let result = self.capture_display(display_index);
265        self.config.webp_config = original_config;
266        result
267    }
268
269    /// Create a streaming pipeline for continuous capture
270    pub fn create_streaming_pipeline(&self) -> StreamingPipelineBuilder {
271        StreamingPipelineBuilder::new()
272    }
273
274    /// Set the capture configuration
275    pub fn set_config(&mut self, config: CaptureConfig) {
276        self.config = config;
277    }
278
279    /// Get the current capture configuration
280    pub fn config(&self) -> &CaptureConfig {
281        &self.config
282    }
283
284    /// Get performance statistics
285    pub fn stats(&self) -> &PerformanceStats {
286        &self.stats
287    }
288
289    /// Reset performance statistics
290    pub fn reset_stats(&mut self) {
291        self.stats = PerformanceStats::default();
292    }
293
294    /// Get memory pool statistics
295    pub fn memory_stats(&self) -> memory_pool::PoolStats {
296        self.memory_pool.stats()
297    }
298
299    /// Get zero-copy statistics
300    pub fn zero_copy_stats(&self) -> Option<pipeline::zero_copy::ZeroCopyStats> {
301        self.zero_copy.as_ref().map(|zc| zc.stats())
302    }
303
304    /// Get the implementation name
305    pub fn implementation_name(&self) -> String {
306        self.capturer.implementation_name()
307    }
308
309    /// Check if hardware acceleration is available
310    pub fn is_hardware_accelerated(&self) -> bool {
311        self.capturer.is_hardware_accelerated()
312    }
313
314    /// Get GPU encoder information
315    pub fn gpu_info(&self) -> Option<String> {
316        self.gpu_encoder.as_ref().map(|gpu| {
317            format!(
318                "{} - {}",
319                gpu.backend_name(),
320                gpu.device_info().unwrap_or_else(|| "Unknown".to_string())
321            )
322        })
323    }
324}
325
326impl Default for WebPScreenshot {
327    fn default() -> Self {
328        Self::new().expect("Failed to initialize WebPScreenshot")
329    }
330}
331
332/// Convenience function to capture the primary display
333pub fn capture_primary_display() -> CaptureResult<Screenshot> {
334    let mut screenshot = WebPScreenshot::new()?;
335    screenshot.capture_display(0)
336}
337
338/// Convenience function to capture with specific quality
339pub fn capture_with_quality(display_index: usize, quality: u8) -> CaptureResult<Screenshot> {
340    let config = CaptureConfig {
341        webp_config: WebPConfig {
342            quality,
343            ..Default::default()
344        },
345        ..Default::default()
346    };
347
348    let mut screenshot = WebPScreenshot::with_config(config)?;
349    screenshot.capture_display(display_index)
350}
351
352/// Get available displays
353pub fn get_displays() -> CaptureResult<Vec<DisplayInfo>> {
354    let capturer = Capturer::new()?;
355    capturer.get_displays()
356}
357
358/// Library version information
359pub fn version() -> &'static str {
360    env!("CARGO_PKG_VERSION")
361}
362
363/// Get library capabilities
364pub fn capabilities() -> String {
365    let mut caps = Vec::new();
366
367    // Platform
368    #[cfg(target_os = "windows")]
369    caps.push("Windows");
370    #[cfg(target_os = "macos")]
371    caps.push("macOS");
372    #[cfg(target_os = "linux")]
373    caps.push("Linux");
374
375    // SIMD
376    let simd_caps = encoder::simd::global_simd_converter().capabilities();
377    if !simd_caps.is_empty() && simd_caps != "None (scalar)" {
378        caps.push(&simd_caps);
379    }
380
381    // Features
382    if ZeroCopyOptimizer::is_supported() {
383        caps.push("Zero-Copy");
384    }
385
386    #[cfg(feature = "gpu")]
387    caps.push("GPU");
388
389    #[cfg(feature = "parallel")]
390    caps.push("Parallel");
391
392    caps.join(", ")
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    #[test]
400    fn test_version() {
401        let version = version();
402        assert!(!version.is_empty());
403    }
404
405    #[test]
406    fn test_capabilities() {
407        let caps = capabilities();
408        println!("Library capabilities: {}", caps);
409        assert!(!caps.is_empty());
410    }
411
412    #[test]
413    fn test_screenshot_creation() {
414        // This test might fail on systems without display access
415        let result = WebPScreenshot::new();
416        // Just check that creation doesn't panic
417        drop(result);
418    }
419
420    #[test]
421    fn test_config_creation() {
422        let config = CaptureConfig::default();
423        assert_eq!(config.webp_config.quality, 80);
424        assert!(!config.include_cursor);
425    }
426}