Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.6] 2026-03-04

### Fixed

- **Incorrect color channels with `wl_shm::Argb8888`**: Fixed red/blue channel swap in screenshots on setups where screencopy reports `Argb8888` (e.g. AMD/Hyprland) [#14](https://github.com/shikoucore/grim-rs/issues/14) (thx [Windblows2000](https://github.com/Windblows2000)) by converting `BGRA` memory layout to crate-level `RGBA`..

### Changed

- **Unified shm format conversion**: Centralized `wl_shm` → `RGBA` byte conversion in a single internal helper and reused it in both single-output and multi-output capture paths to keep behavior consistent.
- **Format handling parity**: Aligned default format fallback behavior between single and multi-output capture paths.

## [0.1.5] 2026-02-06

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "grim-rs"
version = "0.1.5"
version = "0.1.6"
edition = "2021"
description = "Rust implementation of grim screenshot utility for Wayland"
license-file = "LICENSE"
Expand Down
42 changes: 6 additions & 36 deletions src/wayland_capture/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,22 +192,7 @@ impl WaylandCapture {
}

let mut buffer_data = mmap.to_vec();
match format {
ShmFormat::Xrgb8888 => {
for chunk in buffer_data.chunks_exact_mut(4) {
let b = chunk[0];
let g = chunk[1];
let r = chunk[2];
chunk[0] = r;
chunk[1] = g;
chunk[2] = b;
chunk[3] = 255;
}
}
ShmFormat::Argb8888 => {}
_ => {}
}

convert_shm_to_rgba(&mut buffer_data, format);
let output_id = output.id().protocol_id();
let mut final_data = buffer_data;
let mut final_width = width;
Expand Down Expand Up @@ -641,27 +626,12 @@ impl WaylandCapture {
let state = lock_frame_state(frame_state)?;
(state.width, state.height)
};
let mut buffer_data = mmap.to_vec();
if let Some(format) = {
let format = {
let state = lock_frame_state(frame_state)?;
state.format
} {
match format {
ShmFormat::Xrgb8888 => {
for chunk in buffer_data.chunks_exact_mut(4) {
let b = chunk[0];
let g = chunk[1];
let r = chunk[2];
chunk[0] = r;
chunk[1] = g;
chunk[2] = b;
chunk[3] = 255;
}
}
ShmFormat::Argb8888 => {}
_ => {}
}
}
state.format.unwrap_or(ShmFormat::Xrgb8888)
};
let mut buffer_data = mmap.to_vec();
convert_shm_to_rgba(&mut buffer_data, format);
results.insert(
output_name,
CaptureResult {
Expand Down
41 changes: 41 additions & 0 deletions src/wayland_capture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,47 @@ pub(super) fn lock_frame_state(
.map_err(|e| Error::FrameCapture(format!("Frame state mutex poisoned: {}", e)))
}

/// `wl_shm` 32-bit frame bytes to the crate's internal RGBA layout.
///
/// Why this exists:
/// - `CaptureResult` data is exposed as RGBA.
/// - `wl_shm` formats (`Xrgb8888`, `Argb8888`, etc.) are word-defined and on
/// little-endian systems their byte order in memory is not always RGBA.
///
/// Where this is used:
/// - `capture_region_for_output()` after reading the mmap buffer.
/// - `capture_outputs()` for each captured output buffer.
///
/// Notes:
/// - Conversion is done in-place (no extra allocation).
/// - Unsupported formats are left unchanged.
pub(super) fn convert_shm_to_rgba(buffer_data: &mut [u8], format: ShmFormat) {
match format {
// x:R:G:B -> bytes in memory are B,G,R,x on little-endian.
ShmFormat::Xrgb8888 => {
for chunk in buffer_data.chunks_exact_mut(4) {
chunk.swap(0, 2);
chunk[3] = 255;
}
}
// A:R:G:B -> bytes in memory are B,G,R,A on little-endian.
ShmFormat::Argb8888 => {
for chunk in buffer_data.chunks_exact_mut(4) {
chunk.swap(0, 2);
}
}
// x:B:G:R -> bytes in memory are R,G,B,x on little-endian.
ShmFormat::Xbgr8888 => {
for chunk in buffer_data.chunks_exact_mut(4) {
chunk[3] = 255;
}
}
// A:B:G:R -> bytes in memory are R,G,B,A on little-endian.
ShmFormat::Abgr8888 => {}
_ => {}
}
}

/// Guess logical geometry from physical geometry when xdg_output is not available.
pub(super) fn guess_output_logical_geometry(info: &mut OutputInfo) {
info.logical_x = info.x;
Expand Down
38 changes: 38 additions & 0 deletions tests/test.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
use grim_rs::{Box as GrimBox, CaptureParameters, CaptureResult, Error, Grim};
use std::collections::HashMap;
use wayland_client::protocol::wl_shm::Format as ShmFormat;

fn convert_shm_to_rgba_for_test(buffer_data: &mut [u8], format: ShmFormat) {
match format {
ShmFormat::Xrgb8888 => {
for chunk in buffer_data.chunks_exact_mut(4) {
chunk.swap(0, 2);
chunk[3] = 255;
}
}
ShmFormat::Argb8888 => {
for chunk in buffer_data.chunks_exact_mut(4) {
chunk.swap(0, 2);
}
}
ShmFormat::Xbgr8888 => {
for chunk in buffer_data.chunks_exact_mut(4) {
chunk[3] = 255;
}
}
ShmFormat::Abgr8888 => {}
_ => {}
}
}

#[test]
fn test_box_struct_creation() {
Expand Down Expand Up @@ -115,6 +139,20 @@ fn test_image_data_format() {
assert_eq!(data[3], 255); // A
}

#[test]
fn test_convert_xrgb8888_to_rgba() {
let mut pixel_data = vec![10, 20, 30, 99];
convert_shm_to_rgba_for_test(&mut pixel_data, ShmFormat::Xrgb8888);
assert_eq!(pixel_data, vec![30, 20, 10, 255]);
}

#[test]
fn test_convert_argb8888_to_rgba_preserves_alpha() {
let mut pixel_data = vec![10, 20, 30, 40];
convert_shm_to_rgba_for_test(&mut pixel_data, ShmFormat::Argb8888);
assert_eq!(pixel_data, vec![30, 20, 10, 40]);
}

#[test]
fn test_png_compression_levels() {
let test_data = vec![255u8; 100 * 100 * 4]; // 100x100 image
Expand Down