Skip to main content

grim_rs/
lib.rs

1//! # grim-rs
2//!
3//! A pure Rust implementation of the grim screenshot utility for Wayland.
4//!
5//! This library provides a simple interface for taking screenshots on Wayland
6//! compositors that support the `wlr-screencopy` protocol.
7//!
8//! ## Features
9//!
10//! - Capture entire screen (all outputs)
11//! - Capture specific output by name
12//! - Capture specific region
13//! - Capture multiple outputs with different parameters
14//! - Save screenshots as PNG or JPEG
15//! - Get screenshot data as PNG or JPEG bytes
16//!
17//! ## Example
18//!
19//! ```rust,no_run
20//! use grim_rs::Grim;
21//! use chrono::Local;
22//!
23//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
24//! let mut grim = Grim::new()?;
25//! let result = grim.capture_all()?;
26//!
27//! // Generate timestamped filename (like grim-rs does by default)
28//! let filename = format!("{}_grim.png", Local::now().format("%Y%m%d_%Hh%Mm%Ss"));
29//! grim.save_png(result.data(), result.width(), result.height(), &filename)?;
30//! # Ok(())
31//! # }
32//! ```
33
34pub mod error;
35pub mod geometry;
36pub mod pixel_format;
37
38mod buffer;
39mod compositor;
40mod platform;
41mod scaling;
42mod transform;
43
44pub use error::{Error, Result};
45pub use geometry::Region;
46
47#[doc(hidden)]
48pub use buffer::checked_buffer_size;
49#[cfg(target_os = "windows")]
50#[doc(hidden)]
51pub use platform::windows::blend_cursor_rgba;
52#[doc(hidden)]
53pub use transform::{apply_image_transform, rotate_180, rotate_270, rotate_90, OutputTransform};
54
55use platform::Platform;
56
57/// Result of a screenshot capture operation.
58///
59/// Contains the raw image data and dimensions of the captured area.
60#[derive(Debug, Clone)]
61pub struct CaptureResult {
62    /// Raw RGBA image data.
63    ///
64    /// Each pixel is represented by 4 bytes in RGBA format (Red, Green, Blue, Alpha).
65    data: Vec<u8>,
66    /// Width of the captured image in pixels.
67    width: u32,
68    /// Height of the captured image in pixels.
69    height: u32,
70}
71
72impl CaptureResult {
73    pub fn new(data: Vec<u8>, width: u32, height: u32) -> Self {
74        Self {
75            data,
76            width,
77            height,
78        }
79    }
80
81    pub fn data(&self) -> &[u8] {
82        &self.data
83    }
84
85    pub fn width(&self) -> u32 {
86        self.width
87    }
88
89    pub fn height(&self) -> u32 {
90        self.height
91    }
92
93    pub fn into_data(self) -> Vec<u8> {
94        self.data
95    }
96}
97
98/// Information about a display output.
99#[derive(Debug, Clone)]
100pub struct Output {
101    /// Name of the output (e.g., "eDP-1", "HDMI-A-1").
102    name: String,
103    /// Geometry of the output (position and size).
104    geometry: Region,
105    /// Scale factor of the output (e.g., 1 for normal DPI, 2 for HiDPI).
106    scale: i32,
107    /// Description of the output (e.g., monitor model, manufacturer info).
108    description: Option<String>,
109}
110
111impl Output {
112    pub fn name(&self) -> &str {
113        &self.name
114    }
115
116    pub fn geometry(&self) -> &Region {
117        &self.geometry
118    }
119
120    pub fn scale(&self) -> i32 {
121        self.scale
122    }
123
124    pub fn description(&self) -> Option<&str> {
125        self.description.as_deref()
126    }
127}
128
129/// Parameters for capturing a specific output.
130///
131/// Allows specifying different capture parameters for each output when
132///
133/// capturing multiple outputs simultaneously.
134#[derive(Debug, Clone)]
135pub struct CaptureParameters {
136    /// Name of the output to capture.
137    ///
138    /// Must match one of the names returned by [`Grim::get_outputs`].
139    output_name: String,
140    /// Optional region within the output to capture.
141    ///
142    /// If `None`, the entire output will be captured.
143    ///
144    /// If `Some(region)`, only the specified region will be captured.
145    ///
146    /// The region must be within the bounds of the output.
147    region: Option<Region>,
148    /// Whether to include the cursor in the capture.
149    ///
150    /// If `true`, the cursor will be included in the screenshot.
151    ///
152    /// If `false`, the cursor will be excluded from the screenshot.
153    overlay_cursor: bool,
154    /// Scale factor for the output image.
155    ///
156    /// If `None`, uses the default scale (typically the highest output scale).
157    ///
158    /// If `Some(scale)`, the output image will be scaled accordingly.
159    scale: Option<f64>,
160}
161
162impl CaptureParameters {
163    /// Creates a new CaptureParameters with the specified output name.
164    ///
165    /// By default, captures the entire output without cursor and with default scale.
166    pub fn new(output_name: impl Into<String>) -> Self {
167        Self {
168            output_name: output_name.into(),
169            region: None,
170            overlay_cursor: false,
171            scale: None,
172        }
173    }
174
175    /// Sets the region to capture within the output.
176    pub fn region(mut self, region: Region) -> Self {
177        self.region = Some(region);
178        self
179    }
180
181    /// Sets whether to include the cursor in the capture.
182    pub fn overlay_cursor(mut self, overlay_cursor: bool) -> Self {
183        self.overlay_cursor = overlay_cursor;
184        self
185    }
186
187    /// Sets the scale factor for the output image.
188    pub fn scale(mut self, scale: f64) -> Self {
189        self.scale = Some(scale);
190        self
191    }
192
193    /// Returns the output name.
194    pub fn output_name(&self) -> &str {
195        &self.output_name
196    }
197
198    /// Returns the region, if set.
199    pub fn region_ref(&self) -> Option<&Region> {
200        self.region.as_ref()
201    }
202
203    /// Returns whether cursor overlay is enabled.
204    pub fn overlay_cursor_enabled(&self) -> bool {
205        self.overlay_cursor
206    }
207
208    /// Returns the scale factor, if set.
209    pub fn scale_factor(&self) -> Option<f64> {
210        self.scale
211    }
212}
213
214/// Result of capturing multiple outputs.
215///
216/// Contains a map of output names to their respective capture results.
217#[derive(Debug, Clone)]
218pub struct MultiOutputCaptureResult {
219    /// Map of output names to their capture results.
220    ///
221    /// The keys are output names, and the values are the corresponding
222    /// capture results for each output.
223    outputs: std::collections::HashMap<String, CaptureResult>,
224}
225
226impl MultiOutputCaptureResult {
227    /// Creates a new MultiOutputCaptureResult with the given outputs map.
228    pub fn new(outputs: std::collections::HashMap<String, CaptureResult>) -> Self {
229        Self { outputs }
230    }
231
232    /// Gets the capture result for the specified output name.
233    pub fn get(&self, output_name: &str) -> Option<&CaptureResult> {
234        self.outputs.get(output_name)
235    }
236
237    /// Returns a reference to the outputs map.
238    pub fn outputs(&self) -> &std::collections::HashMap<String, CaptureResult> {
239        &self.outputs
240    }
241
242    /// Consumes self and returns the outputs map.
243    pub fn into_outputs(self) -> std::collections::HashMap<String, CaptureResult> {
244        self.outputs
245    }
246}
247
248/// Backend protocol preference for capture initialization.
249#[derive(Debug, Clone, Copy, PartialEq)]
250pub enum Backend {
251    /// Auto-detect: prefer `ext-image-copy-capture-v1`, fall back to `wlr-screencopy`.
252    Auto,
253    /// Force `ext-image-copy-capture-v1` (fails if not available).
254    ExtImageCopyCapture,
255    /// Force `wlr-screencopy` (fails if not available).
256    WlrScreencopy,
257}
258
259/// Main interface for taking screenshots.
260///
261/// Provides methods for capturing screenshots of the entire screen,
262/// specific outputs, regions, or multiple outputs with different parameters.
263pub struct Grim {
264    platform: Platform,
265}
266
267impl Grim {
268    /// Create a new Grim instance with auto-detected backend.
269    ///
270    /// Establishes a connection to the Wayland compositor and initializes
271    /// the necessary protocols for screen capture. Prefers
272    /// `ext-image-copy-capture-v1` when available, falling back to
273    /// `wlr-screencopy`.
274    ///
275    /// Use [`Grim::new_ext`] or [`Grim::new_wlr`] to force a specific backend.
276    ///
277    /// # Errors
278    ///
279    /// Returns an error if:
280    /// - Cannot connect to the Wayland compositor
281    /// - No capture protocol is available
282    /// - Other initialization errors occur
283    ///
284    /// # Example
285    ///
286    /// ```rust,no_run
287    /// use grim_rs::Grim;
288    ///
289    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
290    /// let grim = Grim::new()?;
291    /// # Ok(())
292    /// # }
293    /// ```
294    pub fn new() -> Result<Self> {
295        let inner = Platform::new()?;
296        Ok(Self { platform: inner })
297    }
298
299    /// Create a new Grim instance forcing `ext-image-copy-capture-v1` backend.
300    ///
301    /// **Linux/Wayland only.** On Windows this method returns
302    /// [`Error::UnsupportedProtocol`].
303    ///
304    /// Fails if the compositor does not support this protocol (e.g. KDE, GNOME,
305    /// or older Sway releases). Use [`Grim::new`] for auto-detection if you need
306    /// to support multiple compositors.
307    ///
308    /// # Errors
309    ///
310    /// Returns [`Error::UnsupportedProtocol`] if `ext-image-copy-capture-v1`
311    /// is not available on the compositor, or if called on a non-Linux platform.
312    ///
313    /// # Example
314    ///
315    /// ```rust,no_run
316    /// use grim_rs::Grim;
317    ///
318    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
319    /// // Only works on compositors with ext-image-copy-capture-v1
320    /// // (Sway >= 2025, Hyprland, COSMIC)
321    /// let grim = Grim::new_ext()?;
322    /// # Ok(())
323    /// # }
324    /// ```
325    pub fn new_ext() -> Result<Self> {
326        let inner = Platform::new_with_backend(Backend::ExtImageCopyCapture)?;
327        Ok(Self { platform: inner })
328    }
329
330    /// Create a new Grim instance forcing `wlr-screencopy` backend.
331    ///
332    /// **Linux/Wayland only.** On Windows this method returns
333    /// [`Error::UnsupportedProtocol`].
334    ///
335    /// Fails if the compositor does not support this protocol. Use
336    /// [`Grim::new`] for auto-detection if you need to support multiple
337    /// compositors.
338    ///
339    /// # Errors
340    ///
341    /// Returns [`Error::UnsupportedProtocol`] if `zwlr-screencopy-manager-v1`
342    /// is not available on the compositor, or if called on a non-Linux platform.
343    ///
344    /// # Example
345    ///
346    /// ```rust,no_run
347    /// use grim_rs::Grim;
348    ///
349    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
350    /// // Only works on compositors with wlr-screencopy
351    /// // (older Sway, River, Wayfire, Niri)
352    /// let grim = Grim::new_wlr()?;
353    /// # Ok(())
354    /// # }
355    /// ```
356    pub fn new_wlr() -> Result<Self> {
357        let inner = Platform::new_with_backend(Backend::WlrScreencopy)?;
358        Ok(Self { platform: inner })
359    }
360
361    /// Get information about available display outputs.
362    ///
363    /// Returns a list of all connected display outputs with their names,
364    /// geometries, and scale factors.
365    ///
366    /// # Errors
367    ///
368    /// Returns an error if:
369    /// - No outputs are available
370    /// - Failed to retrieve output information
371    ///
372    /// # Example
373    ///
374    /// ```rust,no_run
375    /// use grim_rs::Grim;
376    ///
377    /// let mut grim = Grim::new()?;
378    /// let outputs = grim.get_outputs()?;
379    ///
380    /// for output in outputs {
381    ///     println!("Output: {} ({}x{})", output.name(), output.geometry().width(), output.geometry().height());
382    /// }
383    /// # Ok::<(), grim_rs::Error>(())
384    /// ```
385    pub fn get_outputs(&mut self) -> Result<Vec<Output>> {
386        self.platform.get_outputs()
387    }
388
389    /// Capture the entire screen (all outputs).
390    ///
391    /// Captures a screenshot that includes all connected display outputs,
392    /// arranged according to their physical positions.
393    ///
394    /// # Errors
395    ///
396    /// Returns an error if:
397    /// - No outputs are available
398    /// - Failed to capture the screen
399    /// - Buffer creation failed
400    ///
401    /// # Example
402    ///
403    /// ```rust,no_run
404    /// use grim_rs::Grim;
405    ///
406    /// let mut grim = Grim::new()?;
407    /// let result = grim.capture_all()?;
408    /// println!("Captured screen: {}x{}", result.width(), result.height());
409    /// # Ok::<(), grim_rs::Error>(())
410    /// ```
411    pub fn capture_all(&mut self) -> Result<CaptureResult> {
412        self.platform.capture_all()
413    }
414
415    /// Capture the entire screen (all outputs) with specified scale factor.
416    ///
417    /// Captures a screenshot that includes all connected display outputs,
418    /// arranged according to their physical positions, with a specified scale factor.
419    ///
420    /// # Arguments
421    ///
422    /// * `scale` - Scale factor for the output image
423    ///
424    /// # Errors
425    ///
426    /// Returns an error if:
427    /// - No outputs are available
428    /// - Failed to capture the screen
429    /// - Buffer creation failed
430    ///
431    /// # Example
432    ///
433    /// ```rust,no_run
434    /// use grim_rs::Grim;
435    ///
436    /// let mut grim = Grim::new()?;
437    /// let result = grim.capture_all_with_scale(1.0)?;
438    /// println!("Captured screen: {}x{}", result.width(), result.height());
439    /// # Ok::<(), grim_rs::Error>(())
440    /// ```
441    pub fn capture_all_with_scale(&mut self, scale: f64) -> Result<CaptureResult> {
442        self.platform.capture_all_with_scale(scale)
443    }
444
445    /// Capture a specific output by name.
446    ///
447    /// Captures a screenshot of the specified display output.
448    ///
449    /// # Arguments
450    ///
451    /// * `output_name` - Name of the output to capture (e.g., "eDP-1", "HDMI-A-1")
452    ///
453    /// # Errors
454    ///
455    /// Returns an error if:
456    /// - The specified output is not found
457    /// - Failed to capture the output
458    /// - Buffer creation failed
459    ///
460    /// # Example
461    ///
462    /// ```rust,no_run
463    /// use grim_rs::Grim;
464    ///
465    /// let mut grim = Grim::new()?;
466    /// // Get available outputs first
467    /// let outputs = grim.get_outputs()?;
468    /// if let Some(output) = outputs.first() {
469    ///     let result = grim.capture_output(output.name())?;
470    ///     println!("Captured output: {}x{}", result.width(), result.height());
471    /// }
472    /// # Ok::<(), grim_rs::Error>(())
473    /// ```
474    pub fn capture_output(&mut self, output_name: &str) -> Result<CaptureResult> {
475        self.platform.capture_output(output_name)
476    }
477
478    /// Capture a specific output by name with specified scale factor.
479    ///
480    /// Captures a screenshot of the specified display output with a specified scale factor.
481    ///
482    /// # Arguments
483    ///
484    /// * `output_name` - Name of the output to capture (e.g., "eDP-1", "HDMI-A-1")
485    /// * `scale` - Scale factor for the output image
486    ///
487    /// # Errors
488    ///
489    /// Returns an error if:
490    /// - The specified output is not found
491    /// - Failed to capture the output
492    /// - Buffer creation failed
493    ///
494    /// # Example
495    ///
496    /// ```rust,no_run
497    /// use grim_rs::Grim;
498    ///
499    /// let mut grim = Grim::new()?;
500    /// // Get available outputs first
501    /// let outputs = grim.get_outputs()?;
502    /// if let Some(output) = outputs.first() {
503    ///     let result = grim.capture_output_with_scale(output.name(), 0.5)?;
504    ///     println!("Captured output at 50% scale: {}x{}", result.width(), result.height());
505    /// }
506    /// # Ok::<(), grim_rs::Error>(())
507    /// ```
508    pub fn capture_output_with_scale(
509        &mut self,
510        output_name: &str,
511        scale: f64,
512    ) -> Result<CaptureResult> {
513        self.platform.capture_output_with_scale(output_name, scale)
514    }
515
516    /// Capture a specific region.
517    ///
518    /// Captures a screenshot of the specified rectangular region.
519    ///
520    /// # Arguments
521    ///
522    /// * `region` - The region to capture, specified as a [`Region`]
523    ///
524    /// # Errors
525    ///
526    /// Returns an error if:
527    /// - No outputs are available
528    /// - Failed to capture the region
529    /// - Buffer creation failed
530    ///
531    /// # Example
532    ///
533    /// ```rust,no_run
534    /// use grim_rs::{Grim, Region};
535    ///
536    /// let mut grim = Grim::new()?;
537    /// // x=100, y=100, width=800, height=600
538    /// let region = Region::new(100, 100, 800, 600);
539    /// let result = grim.capture_region(region)?;
540    /// println!("Captured region: {}x{}", result.width(), result.height());
541    /// # Ok::<(), grim_rs::Error>(())
542    /// ```
543    pub fn capture_region(&mut self, region: Region) -> Result<CaptureResult> {
544        self.platform.capture_region(region)
545    }
546
547    /// Capture a specific region with specified scale factor.
548    ///
549    /// Captures a screenshot of the specified rectangular region with a specified scale factor.
550    ///
551    /// # Arguments
552    ///
553    /// * `region` - The region to capture, specified as a [`Region`]
554    /// * `scale` - Scale factor for the output image
555    ///
556    /// # Errors
557    ///
558    /// Returns an error if:
559    /// - No outputs are available
560    /// - Failed to capture the region
561    /// - Buffer creation failed
562    ///
563    /// # Example
564    ///
565    /// ```rust,no_run
566    /// use grim_rs::{Grim, Region};
567    ///
568    /// let mut grim = Grim::new()?;
569    /// // x=100, y=100, width=800, height=600
570    /// let region = Region::new(100, 100, 800, 600);
571    /// let result = grim.capture_region_with_scale(region, 1.0)?;
572    /// println!("Captured region: {}x{}", result.width(), result.height());
573    /// # Ok::<(), grim_rs::Error>(())
574    /// ```
575    pub fn capture_region_with_scale(
576        &mut self,
577        region: Region,
578        scale: f64,
579    ) -> Result<CaptureResult> {
580        self.platform.capture_region_with_scale(region, scale)
581    }
582
583    /// Capture multiple outputs with different parameters.
584    ///
585    /// Captures screenshots of multiple outputs simultaneously, each with
586    /// potentially different parameters (region, cursor inclusion, etc.).
587    ///
588    /// # Arguments
589    ///
590    /// * `parameters` - Vector of [`CaptureParameters`] specifying what to capture
591    ///   from each output
592    ///
593    /// # Errors
594    ///
595    /// Returns an error if:
596    /// - Any specified output is not found
597    /// - Any specified region is outside the bounds of its output
598    /// - Failed to capture any of the outputs
599    /// - Buffer creation failed
600    ///
601    /// # Example
602    ///
603    /// ```rust,no_run
604    /// use grim_rs::{Grim, CaptureParameters, Region};
605    ///
606    /// let mut grim = Grim::new()?;
607    ///
608    /// // Get available outputs
609    /// let outputs = grim.get_outputs()?;
610    ///
611    /// // Prepare capture parameters for multiple outputs
612    /// let mut parameters = vec![
613    ///     CaptureParameters::new(outputs[0].name())
614    ///         .overlay_cursor(true)
615    /// ];
616    ///
617    /// // If we have a second output, capture a region of it
618    /// if outputs.len() > 1 {
619    ///     let region = Region::new(0, 0, 400, 300);
620    ///     parameters.push(
621    ///         CaptureParameters::new(outputs[1].name())
622    ///             .region(region)
623    ///     );
624    /// }
625    ///
626    /// // Capture all specified outputs
627    /// let results = grim.capture_outputs(parameters)?;
628    /// println!("Captured {} outputs", results.outputs().len());
629    /// # Ok::<(), grim_rs::Error>(())
630    /// ```
631    pub fn capture_outputs(
632        &mut self,
633        parameters: Vec<CaptureParameters>,
634    ) -> Result<MultiOutputCaptureResult> {
635        self.platform.capture_outputs(parameters)
636    }
637
638    /// Capture outputs with scale factor.
639    ///
640    /// Captures screenshots of multiple outputs simultaneously with a specific scale factor.
641    ///
642    /// # Arguments
643    ///
644    /// * `parameters` - Vector of CaptureParameters with scale factors
645    /// * `default_scale` - Default scale factor to use when not specified in parameters
646    ///
647    /// # Errors
648    ///
649    /// Returns an error if any of the outputs can't be captured
650    pub fn capture_outputs_with_scale(
651        &mut self,
652        parameters: Vec<CaptureParameters>,
653        default_scale: f64,
654    ) -> Result<MultiOutputCaptureResult> {
655        self.platform
656            .capture_outputs_with_scale(parameters, default_scale)
657    }
658
659    /// Save captured data as PNG.
660    ///
661    /// Saves the captured image data to a PNG file.
662    ///
663    /// # Arguments
664    ///
665    /// * `data` - Raw RGBA image data from a capture result
666    /// * `width` - Width of the image in pixels
667    /// * `height` - Height of the image in pixels
668    /// * `path` - Path where to save the PNG file
669    ///
670    /// # Errors
671    ///
672    /// Returns an error if:
673    /// - Failed to create or write to the file
674    /// - Image processing failed
675    ///
676    /// # Example
677    ///
678    /// ```rust,no_run
679    /// use grim_rs::Grim;
680    /// use chrono::Local;
681    ///
682    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
683    /// let mut grim = Grim::new()?;
684    /// let result = grim.capture_all()?;
685    ///
686    /// // Generate timestamped filename
687    /// let filename = format!("{}_grim.png", Local::now().format("%Y%m%d_%Hh%Mm%Ss"));
688    /// grim.save_png(result.data(), result.width(), result.height(), &filename)?;
689    /// # Ok(())
690    /// # }
691    /// ```
692    pub fn save_png<P: AsRef<std::path::Path>>(
693        &self,
694        data: &[u8],
695        width: u32,
696        height: u32,
697        path: P,
698    ) -> Result<()> {
699        self.save_png_with_compression(data, width, height, path, 6) // Default compression level of 6
700    }
701
702    /// Save captured data as PNG with compression level control.
703    ///
704    /// Saves the captured image data to a PNG file with specified compression level.
705    ///
706    /// # Arguments
707    ///
708    /// * `data` - Raw RGBA image data from a capture result
709    /// * `width` - Width of the image in pixels
710    /// * `height` - Height of the image in pixels
711    /// * `path` - Path where to save the PNG file
712    /// * `compression` - PNG compression level (0-9, where 9 is highest compression)
713    ///
714    /// # Errors
715    ///
716    /// Returns an error if:
717    /// - Failed to create or write to the file
718    /// - Image processing failed
719    ///
720    /// # Example
721    ///
722    /// ```rust,no_run
723    /// use grim_rs::Grim;
724    /// use chrono::Local;
725    ///
726    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
727    /// let mut grim = Grim::new()?;
728    /// let result = grim.capture_all()?;
729    ///
730    /// // Generate timestamped filename
731    /// let filename = format!("{}_grim.png", Local::now().format("%Y%m%d_%Hh%Mm%Ss"));
732    /// grim.save_png_with_compression(result.data(), result.width(), result.height(), &filename, 9)?;
733    /// # Ok(())
734    /// # }
735    /// ```
736    pub fn save_png_with_compression<P: AsRef<std::path::Path>>(
737        &self,
738        data: &[u8],
739        width: u32,
740        height: u32,
741        path: P,
742        compression: u8,
743    ) -> Result<()> {
744        use std::io::BufWriter;
745
746        let pixels = u64::from(width) * u64::from(height);
747        let expected = pixels.checked_mul(4).ok_or_else(|| {
748            Error::ImageProcessing(image::ImageError::Parameter(
749                image::error::ParameterError::from_kind(
750                    image::error::ParameterErrorKind::DimensionMismatch,
751                ),
752            ))
753        })?;
754        if expected > usize::MAX as u64 {
755            return Err(Error::ImageProcessing(image::ImageError::Parameter(
756                image::error::ParameterError::from_kind(
757                    image::error::ParameterErrorKind::DimensionMismatch,
758                ),
759            )));
760        }
761        if data.len() != expected as usize {
762            return Err(Error::ImageProcessing(image::ImageError::Parameter(
763                image::error::ParameterError::from_kind(
764                    image::error::ParameterErrorKind::DimensionMismatch,
765                ),
766            )));
767        }
768
769        let file = std::fs::File::create(&path).map_err(|e| Error::IoWithContext {
770            operation: format!("creating output file '{}'", path.as_ref().display()),
771            source: e,
772        })?;
773        let writer = BufWriter::new(file);
774        let mut encoder = png::Encoder::new(writer, width, height);
775
776        let compression_level = match compression {
777            0 => png::Compression::Fast,
778            1..=3 => png::Compression::Best,
779            4..=6 => png::Compression::Default,
780            7..=9 => png::Compression::Best,
781            _ => png::Compression::Default,
782        };
783        encoder.set_compression(compression_level);
784
785        encoder.set_color(png::ColorType::Rgba);
786        encoder.set_filter(png::FilterType::NoFilter);
787
788        let mut writer = encoder
789            .write_header()
790            .map_err(|e| Error::Io(std::io::Error::other(format!("PNG encoding error: {}", e))))?;
791
792        writer
793            .write_image_data(data)
794            .map_err(|e| Error::Io(std::io::Error::other(format!("PNG encoding error: {}", e))))?;
795        writer
796            .finish()
797            .map_err(|e| Error::Io(std::io::Error::other(format!("PNG encoding error: {}", e))))?;
798
799        Ok(())
800    }
801
802    /// Save captured data as JPEG.
803    ///
804    /// Saves the captured image data to a JPEG file.
805    ///
806    /// This function is only available when the `jpeg` feature is enabled.
807    ///
808    /// # Arguments
809    ///
810    /// * `data` - Raw RGBA image data from a capture result
811    /// * `width` - Width of the image in pixels
812    /// * `height` - Height of the image in pixels
813    /// * `path` - Path where to save the JPEG file
814    ///
815    /// # Errors
816    ///
817    /// Returns an error if:
818    /// - Failed to create or write to the file
819    /// - Image processing failed
820    /// - JPEG support is not enabled (when feature is disabled)
821    ///
822    /// # Example
823    ///
824    /// ```rust,no_run
825    /// use grim_rs::Grim;
826    /// use chrono::Local;
827    ///
828    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
829    /// let mut grim = Grim::new()?;
830    /// let result = grim.capture_all()?;
831    ///
832    /// // Generate timestamped filename
833    /// let filename = format!("{}_grim.jpg", Local::now().format("%Y%m%d_%Hh%Mm%Ss"));
834    /// grim.save_jpeg(result.data(), result.width(), result.height(), &filename)?;
835    /// # Ok(())
836    /// # }
837    /// ```
838    #[cfg(feature = "jpeg")]
839    pub fn save_jpeg<P: AsRef<std::path::Path>>(
840        &self,
841        data: &[u8],
842        width: u32,
843        height: u32,
844        path: P,
845    ) -> Result<()> {
846        self.save_jpeg_with_quality(data, width, height, path, 80)
847    }
848
849    /// Save captured data as JPEG with quality control.
850    ///
851    /// Saves the captured image data to a JPEG file with specified quality.
852    ///
853    /// This function is only available when the `jpeg` feature is enabled.
854    ///
855    /// # Arguments
856    ///
857    /// * `data` - Raw RGBA image data from a capture result
858    /// * `width` - Width of the image in pixels
859    /// * `height` - Height of the image in pixels
860    /// * `path` - Path where to save the JPEG file
861    /// * `quality` - JPEG quality level (0-100, where 100 is highest quality)
862    ///
863    /// # Errors
864    ///
865    /// Returns an error if:
866    /// - Failed to create or write to the file
867    /// - Image processing failed
868    /// - JPEG support is not enabled (when feature is disabled)
869    ///
870    /// # Example
871    ///
872    /// ```rust,no_run
873    /// use grim_rs::Grim;
874    /// use chrono::Local;
875    ///
876    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
877    /// let mut grim = Grim::new()?;
878    /// let result = grim.capture_all()?;
879    ///
880    /// // Generate timestamped filename
881    /// let filename = format!("{}_grim.jpg", Local::now().format("%Y%m%d_%Hh%Mm%Ss"));
882    /// grim.save_jpeg_with_quality(result.data(), result.width(), result.height(), &filename, 90)?;
883    /// # Ok(())
884    /// # }
885    /// ```
886    #[cfg(feature = "jpeg")]
887    pub fn save_jpeg_with_quality<P: AsRef<std::path::Path>>(
888        &self,
889        data: &[u8],
890        width: u32,
891        height: u32,
892        path: P,
893        quality: u8,
894    ) -> Result<()> {
895        let pixels = u64::from(width) * u64::from(height);
896        let expected = pixels.checked_mul(4).ok_or_else(|| {
897            Error::ImageProcessing(image::ImageError::Parameter(
898                image::error::ParameterError::from_kind(
899                    image::error::ParameterErrorKind::DimensionMismatch,
900                ),
901            ))
902        })?;
903        if expected > usize::MAX as u64 {
904            return Err(Error::ImageProcessing(image::ImageError::Parameter(
905                image::error::ParameterError::from_kind(
906                    image::error::ParameterErrorKind::DimensionMismatch,
907                ),
908            )));
909        }
910        if data.len() != expected as usize {
911            return Err(Error::ImageProcessing(image::ImageError::Parameter(
912                image::error::ParameterError::from_kind(
913                    image::error::ParameterErrorKind::DimensionMismatch,
914                ),
915            )));
916        }
917        let pixels = expected as usize / 4;
918        let rgb_len = pixels.checked_mul(3).ok_or_else(|| {
919            Error::ImageProcessing(image::ImageError::Parameter(
920                image::error::ParameterError::from_kind(
921                    image::error::ParameterErrorKind::DimensionMismatch,
922                ),
923            ))
924        })?;
925        let mut rgb_data = vec![0u8; rgb_len];
926        for (i, rgba) in data.chunks_exact(4).enumerate() {
927            let base = i * 3;
928            rgb_data[base] = rgba[0];
929            rgb_data[base + 1] = rgba[1];
930            rgb_data[base + 2] = rgba[2];
931        }
932
933        let mut output_file = std::fs::File::create(&path).map_err(|e| Error::IoWithContext {
934            operation: format!("creating output file '{}'", path.as_ref().display()),
935            source: e,
936        })?;
937        let mut _encoder = jpeg_encoder::Encoder::new(&mut output_file, quality);
938
939        _encoder
940            .encode(
941                &rgb_data,
942                width as u16,
943                height as u16,
944                jpeg_encoder::ColorType::Rgb,
945            )
946            .map_err(|e| Error::Io(std::io::Error::other(format!("JPEG encoding error: {}", e))))?;
947
948        Ok(())
949    }
950
951    /// Save captured data as JPEG (stub when feature is disabled).
952    ///
953    /// This stub is used when the `jpeg` feature is disabled.
954    ///
955    /// # Errors
956    ///
957    /// Always returns an error indicating that JPEG support is not enabled.
958    #[cfg(not(feature = "jpeg"))]
959    pub fn save_jpeg<P: AsRef<std::path::Path>>(
960        &self,
961        _data: &[u8],
962        _width: u32,
963        _height: u32,
964        _path: P,
965    ) -> Result<()> {
966        Err(Error::ImageProcessing(image::ImageError::Unsupported(
967            image::error::UnsupportedError::from_format_and_kind(
968                image::error::ImageFormatHint::Name("JPEG".to_string()),
969                image::error::UnsupportedErrorKind::Format(image::ImageFormat::Jpeg.into()),
970            ),
971        )))
972    }
973
974    /// Save captured data as JPEG with quality control (stub when feature is disabled).
975    ///
976    /// This stub is used when the `jpeg` feature is disabled.
977    ///
978    /// # Errors
979    ///
980    /// Always returns an error indicating that JPEG support is not enabled.
981    #[cfg(not(feature = "jpeg"))]
982    pub fn save_jpeg_with_quality<P: AsRef<std::path::Path>>(
983        &self,
984        _data: &[u8],
985        _width: u32,
986        _height: u32,
987        _path: P,
988        _quality: u8,
989    ) -> Result<()> {
990        Err(Error::ImageProcessing(image::ImageError::Unsupported(
991            image::error::UnsupportedError::from_format_and_kind(
992                image::error::ImageFormatHint::Name("JPEG".to_string()),
993                image::error::UnsupportedErrorKind::Format(image::ImageFormat::Jpeg.into()),
994            ),
995        )))
996    }
997
998    /// Get image data as JPEG bytes.
999    ///
1000    /// Converts the captured image data to JPEG format and returns the bytes.
1001    ///
1002    /// This function is only available when the `jpeg` feature is enabled.
1003    ///
1004    /// # Arguments
1005    ///
1006    /// * `data` - Raw RGBA image data from a capture result
1007    /// * `width` - Width of the image in pixels
1008    /// * `height` - Height of the image in pixels
1009    ///
1010    /// # Returns
1011    ///
1012    /// Returns the JPEG-encoded image data as a vector of bytes.
1013    ///
1014    /// # Errors
1015    ///
1016    /// Returns an error if:
1017    /// - Image processing failed
1018    /// - JPEG support is not enabled (when feature is disabled)
1019    ///
1020    /// # Example
1021    ///
1022    /// ```rust,no_run
1023    /// use grim_rs::Grim;
1024    ///
1025    /// let mut grim = Grim::new()?;
1026    /// let result = grim.capture_all()?;
1027    /// let jpeg_bytes = grim.to_jpeg(result.data(), result.width(), result.height())?;
1028    /// println!("JPEG data size: {} bytes", jpeg_bytes.len());
1029    /// # Ok::<(), grim_rs::Error>(())
1030    /// ```
1031    #[cfg(feature = "jpeg")]
1032    pub fn to_jpeg(&self, data: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
1033        self.to_jpeg_with_quality(data, width, height, 80)
1034    }
1035
1036    /// Get image data as JPEG bytes with quality control.
1037    ///
1038    /// Converts the captured image data to JPEG format with specified quality and returns the bytes.
1039    ///
1040    /// This function is only available when the `jpeg` feature is enabled.
1041    ///
1042    /// # Arguments
1043    ///
1044    /// * `data` - Raw RGBA image data from a capture result
1045    /// * `width` - Width of the image in pixels
1046    /// * `height` - Height of the image in pixels
1047    /// * `quality` - JPEG quality level (0-100, where 100 is highest quality)
1048    ///
1049    /// # Returns
1050    ///
1051    /// Returns the JPEG-encoded image data as a vector of bytes.
1052    ///
1053    /// # Errors
1054    ///
1055    /// Returns an error if:
1056    /// - Image processing failed
1057    /// - JPEG support is not enabled (when feature is disabled)
1058    ///
1059    /// # Example
1060    ///
1061    /// ```rust,no_run
1062    /// use grim_rs::Grim;
1063    ///
1064    /// let mut grim = Grim::new()?;
1065    /// let result = grim.capture_all()?;
1066    /// let jpeg_bytes = grim.to_jpeg_with_quality(result.data(), result.width(), result.height(), 90)?;
1067    /// println!("JPEG data size: {} bytes", jpeg_bytes.len());
1068    /// # Ok::<(), grim_rs::Error>(())
1069    /// ```
1070    #[cfg(feature = "jpeg")]
1071    pub fn to_jpeg_with_quality(
1072        &self,
1073        data: &[u8],
1074        width: u32,
1075        height: u32,
1076        quality: u8,
1077    ) -> Result<Vec<u8>> {
1078        let pixels = u64::from(width) * u64::from(height);
1079        let expected = pixels.checked_mul(4).ok_or_else(|| {
1080            Error::ImageProcessing(image::ImageError::Parameter(
1081                image::error::ParameterError::from_kind(
1082                    image::error::ParameterErrorKind::DimensionMismatch,
1083                ),
1084            ))
1085        })?;
1086        if expected > usize::MAX as u64 {
1087            return Err(Error::ImageProcessing(image::ImageError::Parameter(
1088                image::error::ParameterError::from_kind(
1089                    image::error::ParameterErrorKind::DimensionMismatch,
1090                ),
1091            )));
1092        }
1093        if data.len() != expected as usize {
1094            return Err(Error::ImageProcessing(image::ImageError::Parameter(
1095                image::error::ParameterError::from_kind(
1096                    image::error::ParameterErrorKind::DimensionMismatch,
1097                ),
1098            )));
1099        }
1100        let pixels = expected as usize / 4;
1101        let rgb_len = pixels.checked_mul(3).ok_or_else(|| {
1102            Error::ImageProcessing(image::ImageError::Parameter(
1103                image::error::ParameterError::from_kind(
1104                    image::error::ParameterErrorKind::DimensionMismatch,
1105                ),
1106            ))
1107        })?;
1108        let mut rgb_data = vec![0u8; rgb_len];
1109        for (i, rgba) in data.chunks_exact(4).enumerate() {
1110            let base = i * 3;
1111            rgb_data[base] = rgba[0];
1112            rgb_data[base + 1] = rgba[1];
1113            rgb_data[base + 2] = rgba[2];
1114        }
1115
1116        let mut jpeg_data = Vec::new();
1117        let mut _encoder = jpeg_encoder::Encoder::new(&mut jpeg_data, quality);
1118
1119        _encoder
1120            .encode(
1121                &rgb_data,
1122                width as u16,
1123                height as u16,
1124                jpeg_encoder::ColorType::Rgb,
1125            )
1126            .map_err(|e| Error::Io(std::io::Error::other(format!("JPEG encoding error: {}", e))))?;
1127
1128        Ok(jpeg_data)
1129    }
1130
1131    /// Get image data as JPEG bytes (stub when feature is disabled).
1132    ///
1133    /// This stub is used when the `jpeg` feature is disabled.
1134    ///
1135    /// # Errors
1136    ///
1137    /// Always returns an error indicating that JPEG support is not enabled.
1138    #[cfg(not(feature = "jpeg"))]
1139    pub fn to_jpeg(&self, _data: &[u8], _width: u32, _height: u32) -> Result<Vec<u8>> {
1140        Err(Error::ImageProcessing(image::ImageError::Unsupported(
1141            image::error::UnsupportedError::from_format_and_kind(
1142                image::error::ImageFormatHint::Name("JPEG".to_string()),
1143                image::error::UnsupportedErrorKind::Format(image::ImageFormat::Jpeg.into()),
1144            ),
1145        )))
1146    }
1147
1148    /// Get image data as JPEG bytes with quality control (stub when feature is disabled).
1149    ///
1150    /// This stub is used when the `jpeg` feature is disabled.
1151    ///
1152    /// # Errors
1153    ///
1154    /// Always returns an error indicating that JPEG support is not enabled.
1155    #[cfg(not(feature = "jpeg"))]
1156    pub fn to_jpeg_with_quality(
1157        &self,
1158        _data: &[u8],
1159        _width: u32,
1160        _height: u32,
1161        _quality: u8,
1162    ) -> Result<Vec<u8>> {
1163        Err(Error::ImageProcessing(image::ImageError::Unsupported(
1164            image::error::UnsupportedError::from_format_and_kind(
1165                image::error::ImageFormatHint::Name("JPEG".to_string()),
1166                image::error::UnsupportedErrorKind::Format(image::ImageFormat::Jpeg.into()),
1167            ),
1168        )))
1169    }
1170
1171    /// Get image data as PNG bytes.
1172    ///
1173    /// Converts the captured image data to PNG format and returns the bytes.
1174    ///
1175    /// # Arguments
1176    ///
1177    /// * `data` - Raw RGBA image data from a capture result
1178    /// * `width` - Width of the image in pixels
1179    /// * `height` - Height of the image in pixels
1180    ///
1181    /// # Returns
1182    ///
1183    /// Returns the PNG-encoded image data as a vector of bytes.
1184    ///
1185    /// # Errors
1186    ///
1187    /// Returns an error if:
1188    /// - Image processing failed
1189    ///
1190    /// # Example
1191    ///
1192    /// ```rust,no_run
1193    /// use grim_rs::Grim;
1194    ///
1195    /// let mut grim = Grim::new()?;
1196    /// let result = grim.capture_all()?;
1197    /// let png_bytes = grim.to_png(result.data(), result.width(), result.height())?;
1198    /// println!("PNG data size: {} bytes", png_bytes.len());
1199    /// # Ok::<(), grim_rs::Error>(())
1200    /// ```
1201    pub fn to_png(&self, data: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
1202        self.to_png_with_compression(data, width, height, 6)
1203    }
1204
1205    /// Get image data as PNG bytes with compression level control.
1206    ///
1207    /// Converts the captured image data to PNG format with specified compression level and returns the bytes.
1208    ///
1209    /// # Arguments
1210    ///
1211    /// * `data` - Raw RGBA image data from a capture result
1212    /// * `width` - Width of the image in pixels
1213    /// * `height` - Height of the image in pixels
1214    /// * `compression` - PNG compression level (0-9, where 9 is highest compression)
1215    ///
1216    /// # Returns
1217    ///
1218    /// Returns the PNG-encoded image data as a vector of bytes.
1219    ///
1220    /// # Errors
1221    ///
1222    /// Returns an error if:
1223    /// - Image processing failed
1224    ///
1225    /// # Example
1226    ///
1227    /// ```rust,no_run
1228    /// use grim_rs::Grim;
1229    ///
1230    /// let mut grim = Grim::new()?;
1231    /// let result = grim.capture_all()?;
1232    /// let png_bytes = grim.to_png_with_compression(result.data(), result.width(), result.height(), 9)?;
1233    /// println!("PNG data size: {} bytes", png_bytes.len());
1234    /// # Ok::<(), grim_rs::Error>(())
1235    /// ```
1236    pub fn to_png_with_compression(
1237        &self,
1238        data: &[u8],
1239        width: u32,
1240        height: u32,
1241        compression: u8,
1242    ) -> Result<Vec<u8>> {
1243        use std::io::Cursor;
1244
1245        let pixels = u64::from(width) * u64::from(height);
1246        let expected = pixels.checked_mul(4).ok_or_else(|| {
1247            Error::ImageProcessing(image::ImageError::Parameter(
1248                image::error::ParameterError::from_kind(
1249                    image::error::ParameterErrorKind::DimensionMismatch,
1250                ),
1251            ))
1252        })?;
1253        if expected > usize::MAX as u64 {
1254            return Err(Error::ImageProcessing(image::ImageError::Parameter(
1255                image::error::ParameterError::from_kind(
1256                    image::error::ParameterErrorKind::DimensionMismatch,
1257                ),
1258            )));
1259        }
1260        if data.len() != expected as usize {
1261            return Err(Error::ImageProcessing(image::ImageError::Parameter(
1262                image::error::ParameterError::from_kind(
1263                    image::error::ParameterErrorKind::DimensionMismatch,
1264                ),
1265            )));
1266        }
1267
1268        let mut output = Vec::new();
1269        {
1270            let writer = Cursor::new(&mut output);
1271            let mut encoder = png::Encoder::new(writer, width, height);
1272
1273            let compression_level = match compression {
1274                0 => png::Compression::Fast,
1275                1..=3 => png::Compression::Best,
1276                4..=6 => png::Compression::Default,
1277                7..=9 => png::Compression::Best,
1278                _ => png::Compression::Default,
1279            };
1280            encoder.set_compression(compression_level);
1281
1282            encoder.set_color(png::ColorType::Rgba);
1283            encoder.set_filter(png::FilterType::NoFilter);
1284
1285            let mut writer = encoder.write_header().map_err(|e| {
1286                Error::Io(std::io::Error::other(format!("PNG encoding error: {}", e)))
1287            })?;
1288
1289            writer.write_image_data(data).map_err(|e| {
1290                Error::Io(std::io::Error::other(format!("PNG encoding error: {}", e)))
1291            })?;
1292            writer.finish().map_err(|e| {
1293                Error::Io(std::io::Error::other(format!("PNG encoding error: {}", e)))
1294            })?;
1295        }
1296
1297        Ok(output)
1298    }
1299
1300    /// Read region from stdin.
1301    ///
1302    /// Reads a region specification from standard input in the format "x,y widthxheight".
1303    ///
1304    /// # Returns
1305    ///
1306    /// Returns a `Region` representing the region read from stdin.
1307    ///
1308    /// # Errors
1309    ///
1310    /// Returns an error if:
1311    /// - Failed to read from stdin
1312    /// - The input format is invalid
1313    ///
1314    /// # Example
1315    ///
1316    /// ```rust,no_run
1317    /// use grim_rs::{Grim, Region};
1318    ///
1319    /// // Parse region from string (same format as stdin would provide)
1320    /// let region = "100,100 800x600".parse::<Region>()?;
1321    /// println!("Region: {}", region);
1322    /// # Ok::<(), grim_rs::Error>(())
1323    /// ```
1324    pub fn read_region_from_stdin() -> Result<Region> {
1325        use std::io::{self, BufRead};
1326
1327        let stdin = io::stdin();
1328        let mut handle = stdin.lock();
1329        let mut line = String::new();
1330
1331        handle.read_line(&mut line)?;
1332
1333        // Remove newline characters
1334        line = line.trim_end().to_string();
1335
1336        line.parse()
1337    }
1338
1339    /// Write image data to stdout as PNG.
1340    ///
1341    /// Writes captured image data directly to standard output in PNG format.
1342    ///
1343    /// # Arguments
1344    ///
1345    /// * `data` - Raw RGBA image data from a capture result
1346    /// * `width` - Width of the image in pixels
1347    /// * `height` - Height of the image in pixels
1348    ///
1349    /// # Errors
1350    ///
1351    /// Returns an error if:
1352    /// - Failed to write to stdout
1353    /// - Image processing failed
1354    ///
1355    /// # Example
1356    ///
1357    /// ```rust,no_run
1358    /// use grim_rs::Grim;
1359    ///
1360    /// let mut grim = Grim::new()?;
1361    /// let result = grim.capture_all()?;
1362    /// grim.write_png_to_stdout(result.data(), result.width(), result.height())?;
1363    /// # Ok::<(), grim_rs::Error>(())
1364    /// ```
1365    pub fn write_png_to_stdout(&self, data: &[u8], width: u32, height: u32) -> Result<()> {
1366        let png_data = self.to_png(data, width, height)?;
1367        use std::io::Write;
1368        let stdout = std::io::stdout();
1369        let mut handle = stdout.lock();
1370        handle.write_all(&png_data)?;
1371        handle.flush()?;
1372        Ok(())
1373    }
1374
1375    /// Write image data to stdout as PNG with compression level.
1376    ///
1377    /// Writes captured image data directly to standard output in PNG format with specified compression.
1378    ///
1379    /// # Arguments
1380    ///
1381    /// * `data` - Raw RGBA image data from a capture result
1382    /// * `width` - Width of the image in pixels
1383    /// * `height` - Height of the image in pixels
1384    /// * `compression` - PNG compression level (0-9, where 9 is highest compression)
1385    ///
1386    /// # Errors
1387    ///
1388    /// Returns an error if:
1389    /// - Failed to write to stdout
1390    /// - Image processing failed
1391    ///
1392    /// # Example
1393    ///
1394    /// ```rust,no_run
1395    /// use grim_rs::Grim;
1396    ///
1397    /// let mut grim = Grim::new()?;
1398    /// let result = grim.capture_all()?;
1399    /// grim.write_png_to_stdout_with_compression(result.data(), result.width(), result.height(), 6)?;
1400    /// # Ok::<(), grim_rs::Error>(())
1401    /// ```
1402    pub fn write_png_to_stdout_with_compression(
1403        &self,
1404        data: &[u8],
1405        width: u32,
1406        height: u32,
1407        compression: u8,
1408    ) -> Result<()> {
1409        let png_data = self.to_png_with_compression(data, width, height, compression)?;
1410        use std::io::Write;
1411        let stdout = std::io::stdout();
1412        let mut handle = stdout.lock();
1413        handle.write_all(&png_data)?;
1414        handle.flush()?;
1415        Ok(())
1416    }
1417
1418    /// Write image data to stdout as JPEG.
1419    ///
1420    /// Writes captured image data directly to standard output in JPEG format.
1421    ///
1422    /// # Arguments
1423    ///
1424    /// * `data` - Raw RGBA image data from a capture result
1425    /// * `width` - Width of the image in pixels
1426    /// * `height` - Height of the image in pixels
1427    ///
1428    /// # Errors
1429    ///
1430    /// Returns an error if:
1431    /// - Failed to write to stdout
1432    /// - Image processing failed
1433    /// - JPEG support is not enabled
1434    ///
1435    /// # Example
1436    ///
1437    /// ```rust,no_run
1438    /// use grim_rs::Grim;
1439    ///
1440    /// let mut grim = Grim::new()?;
1441    /// let result = grim.capture_all()?;
1442    /// grim.write_jpeg_to_stdout(result.data(), result.width(), result.height())?;
1443    /// # Ok::<(), grim_rs::Error>(())
1444    /// ```
1445    #[cfg(feature = "jpeg")]
1446    pub fn write_jpeg_to_stdout(&self, data: &[u8], width: u32, height: u32) -> Result<()> {
1447        self.write_jpeg_to_stdout_with_quality(data, width, height, 80)
1448    }
1449
1450    /// Write image data to stdout as JPEG with quality control.
1451    ///
1452    /// Writes captured image data directly to standard output in JPEG format with specified quality.
1453    ///
1454    /// # Arguments
1455    ///
1456    /// * `data` - Raw RGBA image data from a capture result
1457    /// * `width` - Width of the image in pixels
1458    /// * `height` - Height of the image in pixels
1459    /// * `quality` - JPEG quality level (0-100, where 100 is highest quality)
1460    ///
1461    /// # Errors
1462    ///
1463    /// Returns an error if:
1464    /// - Failed to write to stdout
1465    /// - Image processing failed
1466    /// - JPEG support is not enabledvidence: lib.rs Symbol: to_png_with_compression
1467    ///
1468    /// # Example
1469    ///
1470    /// ```rust,no_run
1471    /// use grim_rs::Grim;
1472    ///
1473    /// let mut grim = Grim::new()?;
1474    /// let result = grim.capture_all()?;
1475    /// grim.write_jpeg_to_stdout_with_quality(result.data(), result.width(), result.height(), 90)?;
1476    /// # Ok::<(), grim_rs::Error>(())
1477    /// ```
1478    #[cfg(feature = "jpeg")]
1479    pub fn write_jpeg_to_stdout_with_quality(
1480        &self,
1481        data: &[u8],
1482        width: u32,
1483        height: u32,
1484        quality: u8,
1485    ) -> Result<()> {
1486        let jpeg_data = self.to_jpeg_with_quality(data, width, height, quality)?;
1487        use std::io::Write;
1488        let stdout = std::io::stdout();
1489        let mut handle = stdout.lock();
1490        handle.write_all(&jpeg_data)?;
1491        handle.flush()?;
1492        Ok(())
1493    }
1494}