6 releases
| new 0.1.5 | Feb 6, 2026 |
|---|---|
| 0.1.4 | Jan 24, 2026 |
| 0.1.3 | Oct 10, 2025 |
| 0.1.0 | Sep 23, 2025 |
#557 in Images
35 downloads per month
Used in hyprshot-rs
170KB
3K
SLoC
grim-rs
if you like this project, then the best way to express gratitude is to give it a star ⭐, it doesn't cost you anything, but I understand that I'm moving the project in the right direction.
Rust implementation of grim-rs screenshot utility for Wayland compositors.
See CHANGELOG.md for version changes. If a release requires migration, it will be documented in MIGRATION.md and referenced from the changelog.
Features
- Pure Rust implementation - no external dependencies on C libraries
- Native Wayland protocol support via
wayland-client - Multi-monitor support with automatic compositing across monitor boundaries
- Output transforms - full support for rotated/flipped displays (all 8 Wayland transform types)
- High-quality image scaling - 4-tier adaptive algorithm selection:
- Upscaling (>1.0): Nearest filter for fast, pixel-accurate enlargement
- Mild downscaling (0.75-1.0): Triangle for fast, high-quality results
- Moderate downscaling (0.5-0.75): CatmullRom for sharp results with good performance
- Heavy downscaling (<0.5): Lanczos3 for best quality at extreme reduction
- Region-based screenshot capture with pixel-perfect accuracy
- Multiple output formats:
- PNG with configurable compression (0-9)
- JPEG with quality control (0-100)
- PPM (uncompressed)
- XDG Pictures directory support - automatic file placement in the XDG Pictures directory when it exists
- Y-invert flag handling - correct screenshot orientation on all compositors
- Cursor overlay support (compositor-dependent)
- Zero external tool dependencies
- Comprehensive API documentation with examples
Usage
As a Library
Add to your Cargo.toml:
[dependencies]
grim-rs = "{version}"
MSRV: Rust 1.68+
Upgrading? See the notes at the top of this README.
Basic Capture Operations
use grim_rs::{Grim, Box};
fn main() -> grim_rs::Result<()> {
let mut grim = Grim::new()?;
// Capture entire screen (all outputs)
let result = grim.capture_all()?;
grim.save_png(result.data(), result.width(), result.height(), "screenshot.png")?;
// Capture specific region (automatically composites across monitors)
let region = Box::new(100, 100, 800, 600);
let result = grim.capture_region(region)?;
grim.save_png(result.data(), result.width(), result.height(), "region.png")?;
// Capture specific output by name (handles transforms/rotation automatically)
let result = grim.capture_output("DP-1")?;
grim.save_png(result.data(), result.width(), result.height(), "output.png")?;
Ok(())
}
Getting Output Information
use grim_rs::Grim;
fn main() -> grim_rs::Result<()> {
let mut grim = Grim::new()?;
// Get list of available outputs with their properties
let outputs = grim.get_outputs()?;
for output in outputs {
println!("Output: {}", output.name());
println!(" Position: ({}, {})", output.geometry().x(), output.geometry().y());
println!(" Size: {}x{}", output.geometry().width(), output.geometry().height());
println!(" Scale: {}", output.scale());
if let Some(desc) = output.description() {
println!(" Description: {}", desc);
}
}
Ok(())
}
Capture with Scaling
use grim_rs::{Grim, Box};
fn main() -> grim_rs::Result<()> {
let mut grim = Grim::new()?;
// Capture entire screen with scaling (high-quality downscaling)
let result = grim.capture_all_with_scale(0.5)?; // 50% size, uses Lanczos3 filter
grim.save_png(result.data(), result.width(), result.height(), "thumbnail.png")?;
// Capture region with scaling
let region = Box::new(0, 0, 1920, 1080);
let result = grim.capture_region_with_scale(region, 0.8)?; // 80% size, uses Triangle filter
grim.save_png(result.data(), result.width(), result.height(), "scaled.png")?;
// Capture specific output with scaling
let result = grim.capture_output_with_scale("DP-1", 0.5)?;
grim.save_png(result.data(), result.width(), result.height(), "output_scaled.png")?;
Ok(())
}
Multiple Output Capture
use grim_rs::{Grim, Box, CaptureParameters};
fn main() -> grim_rs::Result<()> {
let mut grim = Grim::new()?;
// Capture multiple outputs with different parameters
let parameters = vec![
CaptureParameters::new("DP-1")
.overlay_cursor(true),
CaptureParameters::new("HDMI-A-1")
.region(Box::new(0, 0, 1920, 1080))
];
// Scaling is applied uniformly to all outputs via default_scale
let results = grim.capture_outputs_with_scale(parameters, 0.5)?;
for (output_name, result) in results.into_outputs() {
let filename = format!("{}.png", output_name);
grim.save_png(result.data(), result.width(), result.height(), &filename)?;
}
Ok(())
}
Saving to Different Formats
use grim_rs::Grim;
fn main() -> grim_rs::Result<()> {
let mut grim = Grim::new()?;
let result = grim.capture_all()?;
// Save as PNG with default compression (level 6)
grim.save_png(result.data(), result.width(), result.height(), "screenshot.png")?;
// Save as PNG with custom compression (0-9, where 9 is highest)
grim.save_png_with_compression(result.data(), result.width(), result.height(), "compressed.png", 9)?;
// Save as JPEG with default quality (80)
grim.save_jpeg(result.data(), result.width(), result.height(), "screenshot.jpg")?;
// Save as JPEG with custom quality (0-100, where 100 is highest)
grim.save_jpeg_with_quality(result.data(), result.width(), result.height(), "quality.jpg", 95)?;
// Save as PPM (uncompressed)
grim.save_ppm(result.data(), result.width(), result.height(), "screenshot.ppm")?;
Ok(())
}
Converting to Bytes (without saving to file)
use grim_rs::Grim;
fn main() -> grim_rs::Result<()> {
let mut grim = Grim::new()?;
let result = grim.capture_all()?;
// Convert to PNG bytes
let png_bytes = grim.to_png(result.data(), result.width(), result.height())?;
println!("PNG size: {} bytes", png_bytes.len());
// Convert to PNG bytes with custom compression
let png_bytes = grim.to_png_with_compression(result.data(), result.width(), result.height(), 9)?;
// Convert to JPEG bytes
let jpeg_bytes = grim.to_jpeg(result.data(), result.width(), result.height())?;
println!("JPEG size: {} bytes", jpeg_bytes.len());
// Convert to JPEG bytes with custom quality
let jpeg_bytes = grim.to_jpeg_with_quality(result.data(), result.width(), result.height(), 85)?;
// Convert to PPM bytes
let ppm_bytes = grim.to_ppm(result.data(), result.width(), result.height())?;
println!("PPM size: {} bytes", ppm_bytes.len());
Ok(())
}
Writing to Stdout (for piping)
use grim_rs::Grim;
fn main() -> grim_rs::Result<()> {
let mut grim = Grim::new()?;
let result = grim.capture_all()?;
// Write PNG to stdout
grim.write_png_to_stdout(result.data(), result.width(), result.height())?;
// Write PNG to stdout with custom compression
grim.write_png_to_stdout_with_compression(result.data(), result.width(), result.height(), 6)?;
// Write JPEG to stdout
grim.write_jpeg_to_stdout(result.data(), result.width(), result.height())?;
// Write JPEG to stdout with custom quality
grim.write_jpeg_to_stdout_with_quality(result.data(), result.width(), result.height(), 90)?;
// Write PPM to stdout
grim.write_ppm_to_stdout(result.data(), result.width(), result.height())?;
Ok(())
}
Reading Region from Stdin
use grim_rs::Grim;
fn main() -> grim_rs::Result<()> {
let mut grim = Grim::new()?;
// Read region specification from stdin (format: "x,y widthxheight")
let region = Grim::read_region_from_stdin()?;
let result = grim.capture_region(region)?;
grim.save_png(result.data(), result.width(), result.height(), "region.png")?;
Ok(())
}
Command Line Usage
The grim-rs binary supports the same functionality as the library API. By default, it saves to the XDG Pictures directory if it exists, otherwise to the current directory, with timestamped filenames.
Available Options:
-h Show help message and quit
-s <factor> Set the output image's scale factor (e.g., 0.5 for 50%)
-g <geometry> Set the region to capture (format: "x,y widthxheight")
-t png|ppm|jpeg Set the output filetype (default: png)
-q <quality> Set the JPEG compression quality (0-100, default: 80)
-l <level> Set the PNG compression level (0-9, default: 6)
-o <output> Set the output name to capture (e.g., "DP-1", "HDMI-A-1")
-c Include cursor in the screenshot
Usage Examples:
# Build the binary first
cargo build --release
# Capture entire screen (saves to XDG Pictures if it exists, else ./<timestamp>.png)
cargo run --bin grim-rs
# Capture with specific filename
cargo run --bin grim-rs -- screenshot.png
# Capture specific region
cargo run --bin grim-rs -- -g "100,100 800x600" region.png
# Capture with scaling (50% size, creates thumbnail)
cargo run --bin grim-rs -- -s 0.5 thumbnail.png
# Capture specific output by name
cargo run --bin grim-rs -- -o DP-1 monitor.png
# Capture with cursor included
cargo run --bin grim-rs -- -c -o DP-1 with_cursor.png
# Save as JPEG with custom quality
cargo run --bin grim-rs -- -t jpeg -q 90 screenshot.jpg
# Save as PNG with maximum compression
cargo run --bin grim-rs -- -l 9 compressed.png
# Save as PPM (uncompressed)
cargo run --bin grim-rs -- -t ppm screenshot.ppm
# Combine options: region + scaling + cursor
cargo run --bin grim-rs -- -g "0,0 1920x1080" -s 0.8 -c scaled_region.png
# Capture to stdout and pipe to another program
cargo run --bin grim-rs -- - > screenshot.png
# Save to custom directory via environment variable
GRIM_DEFAULT_DIR=/tmp cargo run --bin grim-rs
# Read region from stdin
echo "100,100 800x600" | cargo run --bin grim-rs -- -g -
Using the installed binary:
After installation with cargo install grim-rs, you can use it directly:
# Capture entire screen
grim-rs
# All the same options work without 'cargo run'
grim-rs -g "100,100 800x600" -s 0.5 thumbnail.png
grim-rs -o DP-1 -c monitor.png
grim-rs - | wl-copy # Pipe to clipboard
Note: The binary is named grim-rs to avoid conflicts with the original C implementation of grim.
Supported Wayland Protocols
wl_shm- Shared memory bufferszwlr_screencopy_manager_v1- Screenshot capture (wlroots extension)wl_output- Output information
API Reference
Core Methods
Initialization
Grim::new()- Create new Grim instance and connect to Wayland compositor
Getting Display Information
get_outputs()- Get list of available outputs with their properties (name, geometry, scale)
Capture Methods
capture_all()- Capture entire screen (all outputs)capture_all_with_scale(scale: f64)- Capture entire screen with scalingcapture_output(output_name: &str)- Capture specific output by namecapture_output_with_scale(output_name: &str, scale: f64)- Capture output with scalingcapture_region(region: Box)- Capture specific rectangular regioncapture_region_with_scale(region: Box, scale: f64)- Capture region with scalingcapture_outputs(parameters: Vec<CaptureParameters>)- Capture multiple outputs with different parameterscapture_outputs_with_scale(parameters: Vec<CaptureParameters>, default_scale: f64)- Capture multiple outputs with scaling
Saving to Files
save_png(&data, width, height, path)- Save as PNG with default compression (level 6)save_png_with_compression(&data, width, height, path, compression: u8)- Save as PNG with custom compression (0-9)save_jpeg(&data, width, height, path)- Save as JPEG with default quality (80) [requiresjpegfeature]save_jpeg_with_quality(&data, width, height, path, quality: u8)- Save as JPEG with custom quality (0-100) [requiresjpegfeature]save_ppm(&data, width, height, path)- Save as PPM (uncompressed)
Converting to Bytes
to_png(&data, width, height)- Convert to PNG bytes with default compressionto_png_with_compression(&data, width, height, compression: u8)- Convert to PNG bytes with custom compressionto_jpeg(&data, width, height)- Convert to JPEG bytes with default quality [requiresjpegfeature]to_jpeg_with_quality(&data, width, height, quality: u8)- Convert to JPEG bytes with custom quality [requiresjpegfeature]to_ppm(&data, width, height)- Convert to PPM bytes
Writing to Stdout
write_png_to_stdout(&data, width, height)- Write PNG to stdout with default compressionwrite_png_to_stdout_with_compression(&data, width, height, compression: u8)- Write PNG to stdout with custom compressionwrite_jpeg_to_stdout(&data, width, height)- Write JPEG to stdout with default quality [requiresjpegfeature]write_jpeg_to_stdout_with_quality(&data, width, height, quality: u8)- Write JPEG to stdout with custom quality [requiresjpegfeature]write_ppm_to_stdout(&data, width, height)- Write PPM to stdout
Stdin Input
Grim::read_region_from_stdin()- Read region specification from stdin (format: "x,y widthxheight")
Data Structures
CaptureResult
Contains captured image data:
data: Vec<u8>- Raw RGBA image data (4 bytes per pixel)width: u32- Image width in pixelsheight: u32- Image height in pixels
CaptureParameters
Parameters for capturing specific outputs:
output_name: String- Name of the output to captureregion: Option<Box>- Optional region within the outputoverlay_cursor: bool- Whether to include cursor in capturescale: Option<f64>- Stored in parameters but not applied by the backend; usecapture_outputs_with_scale(..., default_scale)for scaling
MultiOutputCaptureResult
Result of capturing multiple outputs:
outputs: HashMap<String, CaptureResult>- Map of output names to their capture results
Output
Information about a display output:
name: String- Output name (e.g., "eDP-1", "HDMI-A-1")geometry: Box- Output position and sizescale: i32- Scale factor (1 for normal DPI, 2 for HiDPI)description: Option<String>- Monitor model and manufacturer information
Box
Rectangular region:
x: i32- X coordinatey: i32- Y coordinatewidth: i32- Widthheight: i32- Height- Can be parsed from string: "x,y widthxheight"
Feature Flags
jpeg- Enable JPEG support (enabled by default)- Adds
save_jpeg*,to_jpeg*, andwrite_jpeg_to_stdout*methods
- Adds
To disable JPEG support:
[dependencies]
grim-rs = { version = "0.1.0", default-features = false }
Full API Documentation
Comprehensive API documentation is available at docs.rs or can be generated locally:
cargo doc --open
Comparison with Original grim
| Feature | Original grim | grim-rs |
|---|---|---|
| Language | C | Rust |
| Dependencies | libpng, pixman, wayland, libjpeg | Pure Rust crates |
| Output formats | PNG, JPEG, PPM | PNG, JPEG, PPM |
| Installation | System package | Rust crate |
| Integration | External process | Library + Binary |
| Memory safety | Manual | Guaranteed by Rust |
| Output transforms | ✅ | ✅ |
| Y-invert handling | ✅ | ✅ |
| Multi-monitor compositing | ✅ | ✅ |
| Image scaling | Nearest-neighbor | 4-tier adaptive (Nearest/Triangle/CatmullRom/Lanczos3) |
| XDG Pictures support | ✅ | ✅ |
| Output descriptions | ✅ | ✅ |
| Color accuracy | ✅ | ✅ |
| Real capture | ✅ | ✅ |
Architecture
┌─────────────────┐
│ Application │
├─────────────────┤
│ grim-rs │
├─────────────────┤
│ wayland-client │
├─────────────────┤
│ Wayland │
│ Compositor │
└─────────────────┘
Key Components
- Grim - Main interface for taking screenshots
- CaptureResult - Contains screenshot data and dimensions
- CaptureParameters - Parameters for multi-output capture
- Buffer - Shared memory buffer management
- Box - Region and coordinate handling
- Output - Monitor information with transform support
- Error - Comprehensive error handling
Image Processing Pipeline
Wayland Screencopy → Buffer → Output Transform → Y-invert → Scaling → Format Conversion → Save
↓ ↓ ↓
(rotation/flip) (vertical) (Bilinear/Lanczos3)
Scaling Quality
Adaptive 4-tier algorithm selection ensures optimal quality/performance balance:
-
Upscaling (scale > 1.0): Nearest filter
- Fastest option for scaling up
- Preserves hard edges (can look blocky)
- Example: 1920×1080 → 2560×1440 (1.33×)
-
Mild downscaling (0.75 ≤ scale ≤ 1.0): Triangle filter
- Fast, high-quality for small size reductions
- Perfect for minor adjustments: 1920×1080 → 1536×864 (0.8×)
-
Moderate downscaling (0.5 ≤ scale < 0.75): CatmullRom filter
- Sharper results than Triangle
- Better performance than Lanczos3
- Ideal for medium reduction: 1920×1080 → 1280×720 (0.67×)
-
Heavy downscaling (scale < 0.5): Lanczos3 convolution
- Best quality for significant reduction
- Ideal for thumbnails: 3840×2160 → 960×540 (0.25×)
- Superior detail preservation at extreme scales
Environment Variables
GRIM_DEFAULT_DIR- Override default screenshot directory (highest priority)XDG_PICTURES_DIR- XDG Pictures directory (from env or~/.config/user-dirs.dirs)
Priority order: GRIM_DEFAULT_DIR → XDG_PICTURES_DIR (if it exists) → current directory
Supported Compositors
- ✅ Hyprland
- ✅ Sway
- ✅ River
- ✅ Wayfire
- ✅ Any wlroots-based compositor with
zwlr_screencopy_manager_v1
Limitations
- Requires compositor with
zwlr_screencopy_manager_v1protocol support - Linux-only (due to shared memory implementation)
- Cursor overlay depends on compositor support
Building
cd grim-rs
cargo build --release
Testing
# Run tests
cargo test
# Run examples
cargo run --example simple all screenshot.png
cargo run --example multi_output
Contributing
- Fork the repository
- Create a feature branch
- Make changes
- Add tests
- Submit a pull request
License
MIT License - see LICENSE file for details.
Dependencies
~14–20MB
~405K SLoC