Files
rustlock/src/screenshot.rs
T
jory c6ec65c851
Code Quality / quality-checks (push) Has been cancelled
Security Scan / security-audit (push) Has been cancelled
chore: add GitHub automation and fix rand vulnerability
GitHub Actions:
- Add Dependabot for weekly dependency updates (cargo, github-actions)
- Add PR labeler workflow with file-based auto-labeling
- Add issue/PR templates (bug report, feature request, PR template)
- Fix release.yml artifact paths and labeler.yml config syntax
Security:
- Upgrade rand 0.8 → 0.10 to fix RUSTSEC-2026-0097 unsoundness
- Update screenshot.rs for rand 0.10 API (thread_rng → rng, gen_range → random_range)
2026-04-11 18:18:41 +02:00

474 lines
16 KiB
Rust

//!
//! This module provides functionality to capture the current screen contents
//! and apply visual effects like blur and vignette, similar to swaylock-effects.
use anyhow::{Context, Result};
use cairo::ImageSurface;
use log::debug;
use smithay_client_toolkit::shm::{slot::Buffer, slot::SlotPool};
use std::sync::Mutex;
use wayland_client::globals::GlobalList;
use wayland_client::protocol::{wl_output, wl_shm};
use wayland_client::{Dispatch, QueueHandle};
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::{
Flags, ZwlrScreencopyFrameV1,
};
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
use crate::config::Config;
/// A captured screenshot with optional visual effects applied.
pub struct Screenshot {
surface: ImageSurface,
}
impl Screenshot {
/// Create a new screenshot from a Cairo surface.
pub fn new(surface: ImageSurface) -> Self {
Self { surface }
}
/// Consume the screenshot and return the underlying Cairo surface.
pub fn into_inner(self) -> ImageSurface {
self.surface
}
/// Apply configured visual effects to the screenshot.
pub fn apply_effects(&mut self, config: &Config) -> Result<()> {
if let Some((radius, times)) = config.effect_blur {
self.apply_blur(radius, times)?;
}
if let Some((base, factor)) = config.effect_vignette {
self.apply_vignette(base, factor);
}
if let Some(pixel_size) = config.effect_pixelate {
self.apply_pixelate(pixel_size);
}
if let Some(angle) = config.effect_swirl {
self.apply_swirl(angle);
}
if let Some(factor) = config.effect_melting {
self.apply_melting(factor);
}
Ok(())
}
/// Apply a swirl effect.
pub fn apply_swirl(&mut self, angle: f32) {
let width = self.surface.width();
let height = self.surface.height();
let center_x = width as f32 / 2.0;
let center_y = height as f32 / 2.0;
let radius = center_x.min(center_y);
let stride = self.surface.stride() as usize;
let mut data = vec![0u8; stride * height as usize];
self.surface
.with_data(|src| data.copy_from_slice(src))
.unwrap();
let original = data.clone();
for y in 0..height {
for x in 0..width {
let dx = x as f32 - center_x;
let dy = y as f32 - center_y;
let d = (dx * dx + dy * dy).sqrt();
if d < radius {
let percent = (radius - d) / radius;
let theta = percent * percent * angle;
let s = theta.sin();
let c = theta.cos();
let nx = (c * dx - s * dy + center_x) as i32;
let ny = (s * dx + c * dy + center_y) as i32;
if nx >= 0 && nx < width && ny >= 0 && ny < height {
let src_idx = (ny as usize * stride) + (nx as usize * 4);
let dst_idx = (y as usize * stride) + (x as usize * 4);
data[dst_idx..dst_idx + 4].copy_from_slice(&original[src_idx..src_idx + 4]);
}
}
}
}
let mut surface_data = self.surface.data().unwrap();
surface_data.copy_from_slice(&data);
}
/// Apply a melting effect (vertical smear).
pub fn apply_melting(&mut self, factor: f32) {
let width = self.surface.width();
let height = self.surface.height();
let stride = self.surface.stride() as usize;
let mut data = vec![0u8; stride * height as usize];
self.surface
.with_data(|src| data.copy_from_slice(src))
.unwrap();
use rand::RngExt;
let mut rng = rand::rng();
for x in 0..width {
let mut melt_amount = 0.0;
for y in 0..height {
melt_amount += rng.random_range(0.0..factor);
let src_y = (y as f32 - melt_amount).max(0.0) as i32;
let src_idx = (src_y as usize * stride) + (x as usize * 4);
let dst_idx = (y as usize * stride) + (x as usize * 4);
// Copy the pixel from above to create a smear
let pixel = [
data[src_idx],
data[src_idx + 1],
data[src_idx + 2],
data[src_idx + 3],
];
data[dst_idx..dst_idx + 4].copy_from_slice(&pixel);
}
}
let mut surface_data = self.surface.data().unwrap();
surface_data.copy_from_slice(&data);
}
/// Pixelate the surface.
pub fn apply_pixelate(&mut self, pixel_size: u32) {
if pixel_size <= 1 {
return;
}
let width = self.surface.width();
let height = self.surface.height();
let stride = self.surface.stride() as usize;
let mut data = vec![0u8; stride * height as usize];
self.surface
.with_data(|src| data.copy_from_slice(src))
.unwrap();
for y in (0..height).step_by(pixel_size as usize) {
for x in (0..width).step_by(pixel_size as usize) {
let mut r = 0u32;
let mut g = 0u32;
let mut b = 0u32;
let mut count = 0u32;
// Average pixels in the block
for py in 0..pixel_size {
for px in 0..pixel_size {
let cur_x = x + px as i32;
let cur_y = y + py as i32;
if cur_x < width && cur_y < height {
let index = (cur_y as usize * stride) + (cur_x as usize * 4);
r += data[index] as u32;
g += data[index + 1] as u32;
b += data[index + 2] as u32;
count += 1;
}
}
}
if count > 0 {
let r = (r / count) as u8;
let g = (g / count) as u8;
let b = (b / count) as u8;
// Fill the block
for py in 0..pixel_size {
for px in 0..pixel_size {
let cur_x = x + px as i32;
let cur_y = y + py as i32;
if cur_x < width && cur_y < height {
let index = (cur_y as usize * stride) + (cur_x as usize * 4);
data[index] = r;
data[index + 1] = g;
data[index + 2] = b;
}
}
}
}
}
}
let mut surface_data = self.surface.data().unwrap();
surface_data.copy_from_slice(&data);
}
/// Apply a Gaussian blur effect.
pub fn apply_blur(&mut self, radius: u32, times: u32) -> Result<()> {
if radius == 0 || times == 0 {
return Ok(());
}
let width = self.surface.width();
let height = self.surface.height();
let stride = self.surface.stride() as usize;
let mut data = vec![0u8; stride * height as usize];
self.surface
.with_data(|src| data.copy_from_slice(src))
.context("Failed to get surface data")?;
// Convert to image::RgbaImage for processing
let mut img: image::ImageBuffer<image::Rgba<u8>, Vec<u8>> =
image::ImageBuffer::from_raw(width as u32, height as u32, data)
.context("Failed to create image buffer")?;
for _ in 0..times {
let mut rgb_data: Vec<[u8; 3]> =
Vec::with_capacity((width as usize) * (height as usize));
for pixel in img.pixels() {
rgb_data.push([pixel[0], pixel[1], pixel[2]]);
}
fastblur::gaussian_blur(
&mut rgb_data,
width as usize,
height as usize,
radius as f32,
);
for (i, pixel) in img.pixels_mut().enumerate() {
pixel[0] = rgb_data[i][0];
pixel[1] = rgb_data[i][1];
pixel[2] = rgb_data[i][2];
}
}
// Copy back to surface
let new_data = img.into_raw();
let mut surface_data = self.surface.data()?;
surface_data.copy_from_slice(&new_data);
Ok(())
}
/// Apply a vignette effect (darken edges).
pub fn apply_vignette(&mut self, base: f32, factor: f32) {
let width = self.surface.width();
let height = self.surface.height();
let center_x = width as f32 / 2.0;
let center_y = height as f32 / 2.0;
let max_distance = (center_x * center_x + center_y * center_y).sqrt();
let stride = self.surface.stride() as usize;
let mut data = vec![0u8; stride * height as usize];
self.surface
.with_data(|src| data.copy_from_slice(src))
.unwrap();
for y in 0..height {
for x in 0..width {
let dx = x as f32 - center_x;
let dy = y as f32 - center_y;
let distance = (dx * dx + dy * dy).sqrt();
let vignette_factor = base + (1.0 - base) * (distance / max_distance).powf(factor);
let index = ((y * width + x) * 4) as usize;
for i in 0..3 {
let value = data[index + i] as f32 * vignette_factor;
data[index + i] = value.clamp(0.0, 255.0) as u8;
}
}
}
let mut surface_data = self.surface.data().unwrap();
surface_data.copy_from_slice(&data);
}
}
#[derive(Clone)]
/// Information about a buffer from the screencopy protocol.
pub struct BufferInfo {
pub width: u32,
pub height: u32,
pub stride: u32,
pub format: wl_shm::Format,
}
/// Handle to a captured buffer that can be converted to a Cairo surface.
pub struct ScreencopyBufferHandle {
pub buffer: Buffer,
pub info: BufferInfo,
pub y_invert: bool,
}
/// Manager for the wlr-screencopy protocol.
pub struct ScreenshotManager {
manager: Option<ZwlrScreencopyManagerV1>,
}
impl ScreenshotManager {
/// Bind to the wlr-screencopy global and create a new manager.
///
/// Returns `Ok(Self)` if the protocol is available, otherwise `Err`.
pub fn new<D>(globals: &GlobalList, qh: &QueueHandle<D>) -> Result<Self>
where
D: Dispatch<ZwlrScreencopyManagerV1, ()> + 'static,
{
let manager = globals
.bind::<ZwlrScreencopyManagerV1, _, _>(qh, 1..=3, ())
.ok();
if manager.is_none() {
debug!("zwlr_screencopy_manager_v1 not available");
}
Ok(Self { manager })
}
/// Initiate a screencopy operation for the given output.
///
/// This method sends a screencopy request and returns the frame object.
/// The frame events will be dispatched to the provided queue's dispatcher
/// with the given user data.
pub fn capture_output<D>(
&self,
output: &wl_output::WlOutput,
qh: &QueueHandle<D>,
user_data: CaptureData,
) -> Result<ZwlrScreencopyFrameV1>
where
D: Dispatch<ZwlrScreencopyFrameV1, CaptureData> + 'static,
{
let manager = self.manager.as_ref().context("Screencopy not available")?;
let frame = manager.capture_output(0, output, qh, user_data);
Ok(frame)
}
/// Convert a captured buffer to a Cairo ImageSurface.
pub fn buffer_to_surface(
&self,
handle: ScreencopyBufferHandle,
pool: &mut SlotPool,
) -> Result<ImageSurface> {
let info = handle.info;
let y_invert = handle.y_invert;
let canvas = handle
.buffer
.canvas(pool)
.context("Failed to get buffer canvas")?;
let pixel_width = (info.width * 4) as usize;
let stride = info.stride as usize;
let height = info.height as usize;
if stride < pixel_width {
anyhow::bail!("Stride smaller than pixel width");
}
let raw_data = {
let mut data = vec![0u8; (info.width * info.height * 4) as usize];
for row in 0..height {
let src_offset = row * stride;
let dst_offset = row * pixel_width;
data[dst_offset..dst_offset + pixel_width]
.copy_from_slice(&canvas[src_offset..src_offset + pixel_width]);
}
data
};
let converted_data = match info.format {
wayland_client::protocol::wl_shm::Format::Argb8888 => raw_data,
wayland_client::protocol::wl_shm::Format::Xbgr8888 => {
convert_xbgr8888_to_argb32(&raw_data, info.width as usize, info.height as usize)
}
wayland_client::protocol::wl_shm::Format::Xrgb8888 => {
convert_xrgb8888_to_argb32(&raw_data, info.width as usize, info.height as usize)
}
_ => {
log::warn!("Unsupported format {:?}, using raw data as-is", info.format);
raw_data
}
};
if y_invert {
let mut flipped = vec![0u8; (info.width * info.height * 4) as usize];
let src_stride = (info.width * 4) as usize;
for row in 0..height {
let src_row = height - 1 - row;
let src_offset = src_row * src_stride;
let dst_offset = row * src_stride;
flipped[dst_offset..dst_offset + src_stride]
.copy_from_slice(&converted_data[src_offset..src_offset + src_stride]);
}
return ImageSurface::create_for_data(
flipped,
cairo::Format::ARgb32,
info.width as i32,
info.height as i32,
src_stride as i32,
)
.context("Failed to create flipped Cairo surface");
}
ImageSurface::create_for_data(
converted_data,
cairo::Format::ARgb32,
info.width as i32,
info.height as i32,
pixel_width as i32,
)
.context("Failed to create Cairo surface")
}
}
/// Convert Xbgr8888 buffer data to ARGB32 format (little-endian byte order).
/// Xbgr8888: 32-bit word 0xXXBBGGRR, memory layout: [R, G, B, X]
/// ARGB32: 32-bit word 0xAARRGGBB, memory layout: [B, G, R, A]
fn convert_xbgr8888_to_argb32(data: &[u8], width: usize, height: usize) -> Vec<u8> {
let mut result = Vec::with_capacity(width * height * 4);
for i in 0..width * height {
let src = i * 4;
// Source: [R, G, B, X] -> Destination: [B, G, R, A=255]
result.push(data[src + 2]); // B
result.push(data[src + 1]); // G
result.push(data[src]); // R
result.push(255); // A
}
result
}
/// Convert Xrgb8888 buffer data to ARGB32 format (little-endian byte order).
/// Xrgb8888: 32-bit word 0xXXRRGGBB, memory layout: [B, G, R, X]
/// ARGB32: 32-bit word 0xAARRGGBB, memory layout: [B, G, R, A]
fn convert_xrgb8888_to_argb32(data: &[u8], width: usize, height: usize) -> Vec<u8> {
let mut result = Vec::with_capacity(width * height * 4);
for i in 0..width * height {
let src = i * 4;
// Source: [B, G, R, X] -> Destination: [B, G, R, A=255]
result.push(data[src]); // B
result.push(data[src + 1]); // G
result.push(data[src + 2]); // R
result.push(255); // A
}
result
}
/// User data associated with a screencopy frame request.
///
/// Stores intermediate data needed to assemble the final screenshot once
/// all frame events are received.
pub struct CaptureData {
pub output_idx: usize,
pub info: Mutex<Option<BufferInfo>>,
pub flags: Mutex<Option<Flags>>,
pub buffer: Mutex<Option<Buffer>>,
pub pool: Mutex<Option<SlotPool>>,
}
impl CaptureData {
/// Create new capture data for the given output index.
pub fn new(output_idx: usize) -> Self {
Self {
output_idx,
info: Mutex::new(None),
flags: Mutex::new(None),
buffer: Mutex::new(None),
pool: Mutex::new(None),
}
}
}