diff --git a/CHANGELOG.md b/CHANGELOG.md index f65aa98..55aafd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 0dcaae6..ca8778b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -569,7 +569,7 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "grim-rs" -version = "0.1.5" +version = "0.1.6" dependencies = [ "chrono", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 54b2731..a0bea8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/wayland_capture/capture.rs b/src/wayland_capture/capture.rs index 3ac8c68..9b77207 100644 --- a/src/wayland_capture/capture.rs +++ b/src/wayland_capture/capture.rs @@ -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; @@ -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 { diff --git a/src/wayland_capture/mod.rs b/src/wayland_capture/mod.rs index be1ab3b..fd9b8fd 100644 --- a/src/wayland_capture/mod.rs +++ b/src/wayland_capture/mod.rs @@ -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; diff --git a/tests/test.rs b/tests/test.rs index 688da1d..ca41511 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -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() { @@ -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