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