From 3495b42afa93f76a3028465faa16319716d53e35 Mon Sep 17 00:00:00 2001 From: elwrcl Date: Sun, 4 Oct 2026 20:44:47 +0300 Subject: [PATCH] stitch: a selection over several outputs as one image --- src/main.rs | 1 + src/stitch.rs | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 src/stitch.rs diff --git a/src/main.rs b/src/main.rs index bdce52f..90fe397 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ mod selection; mod shadow; mod sound; mod stack; +mod stitch; mod theme; mod thumbnail; mod toolbar; diff --git a/src/stitch.rs b/src/stitch.rs new file mode 100644 index 0000000..40b49da --- /dev/null +++ b/src/stitch.rs @@ -0,0 +1,116 @@ +//! A selection over several outputs as one image. + +use image::{RgbaImage, imageops}; + +use crate::frame::Frame; +use crate::selection::Span; + +/// The whole selection: each output's part from its frozen frame at its +/// place (resized when its output's scale is smaller), transparent where +/// no output is. +pub fn stitch(frames: &[Frame], span: &Span) -> RgbaImage { + let mut image = RgbaImage::new(span.size.0, span.size.1); + for part in &span.parts { + let mut crop = frames[part.output].to_rgba(part.rect); + if crop.dimensions() != part.size { + crop = imageops::resize( + &crop, + part.size.0, + part.size.1, + imageops::FilterType::Triangle, + ); + } + imageops::replace(&mut image, &crop, part.at.0 as i64, part.at.1 as i64); + } + image +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use image::RgbaImage; + + use super::*; + use crate::frame::{Frame, OutputGeom, PixelRect}; + use crate::selection::{Part, Span}; + + /// A frame of one colour, given as R, G, B (stored as B, G, R, X). + fn frame(w: u32, h: u32, rgb: [u8; 3]) -> Frame { + Frame { + output: OutputGeom { + name: "X".into(), + x: 0, + y: 0, + width: w as i32, + height: h as i32, + }, + pixels: Arc::new(RgbaImage::from_pixel( + w, + h, + image::Rgba([rgb[2], rgb[1], rgb[0], 0]), + )), + } + } + + fn rect(x: u32, y: u32, width: u32, height: u32) -> PixelRect { + PixelRect { + x, + y, + width, + height, + } + } + + #[test] + fn parts_land_in_place_and_gaps_stay_clear() { + let frames = [frame(10, 10, [255, 0, 0]), frame(10, 10, [0, 0, 255])]; + let span = Span { + size: (8, 6), + parts: vec![ + Part { + output: 0, + rect: rect(0, 0, 4, 3), + at: (4, 3), + size: (4, 3), + }, + Part { + output: 1, + rect: rect(2, 2, 8, 3), + at: (0, 0), + size: (8, 3), + }, + ], + }; + let img = stitch(&frames, &span); + assert_eq!(img.dimensions(), (8, 6)); + assert_eq!( + img.get_pixel(0, 0).0, + [0, 0, 255, 255], + "the monitor's part, opaque" + ); + assert_eq!(img.get_pixel(7, 2).0, [0, 0, 255, 255]); + assert_eq!(img.get_pixel(5, 4).0, [255, 0, 0, 255], "the laptop's part"); + assert_eq!(img.get_pixel(1, 4).0[3], 0, "no output there: transparent"); + } + + #[test] + fn a_smaller_scale_part_is_resized_to_its_place() { + let frames = [frame(4, 4, [0, 255, 0])]; + let span = Span { + size: (8, 8), + parts: vec![Part { + output: 0, + rect: rect(0, 0, 4, 4), + at: (0, 0), + size: (8, 8), + }], + }; + let img = stitch(&frames, &span); + assert_eq!( + img.get_pixel(7, 7).0, + [0, 255, 0, 255], + "filled to the corner" + ); + } +}