From 8e480ae4cb520e81cc7966cb5ebed37d3483f824 Mon Sep 17 00:00:00 2001 From: Charles Johnson Date: Fri, 28 Jul 2023 21:34:14 +0100 Subject: [PATCH] [libwayshot] Mutli-monitor handling built into libwayshot Signed-off-by: c-h-johnson Signed-off-by: decodetalkers Signed-off-by: Shinyzenith --- Cargo.lock | 34 +- libwayshot/Cargo.toml | 3 +- libwayshot/README.md | 12 + libwayshot/src/dispatch.rs | 223 +++++++ libwayshot/src/error.rs | 24 + {wayshot => libwayshot}/src/image_util.rs | 0 libwayshot/src/lib.rs | 768 ++++++++++------------ libwayshot/src/output.rs | 20 + libwayshot/src/screencopy.rs | 137 ++++ wayshot/Cargo.toml | 5 +- wayshot/src/output.rs | 174 ----- wayshot/src/utils.rs | 43 +- wayshot/src/wayshot.rs | 218 +----- 13 files changed, 884 insertions(+), 777 deletions(-) create mode 100644 libwayshot/src/dispatch.rs create mode 100644 libwayshot/src/error.rs rename {wayshot => libwayshot}/src/image_util.rs (100%) create mode 100644 libwayshot/src/output.rs create mode 100644 libwayshot/src/screencopy.rs delete mode 100644 wayshot/src/output.rs diff --git a/Cargo.lock b/Cargo.lock index 69a9f6f2..7e1b7d49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -352,6 +352,7 @@ dependencies = [ "log", "memmap2", "nix", + "thiserror", "wayland-client", "wayland-protocols", "wayland-protocols-wlr", @@ -592,6 +593,17 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tempfile" version = "3.6.0" @@ -615,6 +627,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.7" @@ -728,8 +760,6 @@ dependencies = [ "image", "libwayshot", "log", - "wayland-client", - "wayland-protocols", ] [[package]] diff --git a/libwayshot/Cargo.toml b/libwayshot/Cargo.toml index a49e9073..c447fb1a 100644 --- a/libwayshot/Cargo.toml +++ b/libwayshot/Cargo.toml @@ -9,10 +9,11 @@ version = "0.1.2" edition = "2021" [dependencies] -image = { version = "0.24", default-features = false, features = ["jpeg", "png", "pnm", "qoi"] } +image = { version = "0.24", default-features = false } log = "0.4.19" memmap2 = "0.7.1" nix = "0.26.2" +thiserror = "1" wayland-client = "0.30.2" wayland-protocols = { version = "0.30.0", features=["client", "unstable"] } wayland-protocols-wlr = { version = "0.1.0", features = ["client"] } diff --git a/libwayshot/README.md b/libwayshot/README.md index 5936005d..a18983c7 100644 --- a/libwayshot/README.md +++ b/libwayshot/README.md @@ -8,3 +8,15 @@
( + frame_format: &FrameFormat, + frame_mmap: &MmapMut, +) -> Result>> +where + P: Pixel, +{ + ImageBuffer::from_vec(frame_format.width, frame_format.height, frame_mmap.to_vec()) + .ok_or(Error::BufferTooSmall) +} + +/// The copied frame comprising of the FrameFormat, ColorType (Rgba8), and a memory backed shm +/// file that holds the image data in it. +#[derive(Debug)] +pub struct FrameCopy { + pub frame_format: FrameFormat, + pub frame_color_type: ColorType, + pub frame_mmap: MmapMut, + pub transform: wl_output::Transform, +} + +impl TryFrom<&FrameCopy> for DynamicImage { + type Error = Error; + + fn try_from(value: &FrameCopy) -> Result { + Ok(match value.frame_color_type { + ColorType::Rgb8 => DynamicImage::ImageRgb8(create_image_buffer( + &value.frame_format, + &value.frame_mmap, + )?), + ColorType::Rgba8 => DynamicImage::ImageRgba8(create_image_buffer( + &value.frame_format, + &value.frame_mmap, + )?), + _ => return Err(Error::InvalidColor), + }) + } +} + +/// Return a RawFd to a shm file. We use memfd create on linux and shm_open for BSD support. +/// You don't need to mess around with this function, it is only used by +/// capture_output_frame. +pub fn create_shm_fd() -> std::io::Result { + // Only try memfd on linux and freebsd. + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + loop { + // Create a file that closes on succesful execution and seal it's operations. + match memfd::memfd_create( + CStr::from_bytes_with_nul(b"libwayshot\0").unwrap(), + memfd::MemFdCreateFlag::MFD_CLOEXEC | memfd::MemFdCreateFlag::MFD_ALLOW_SEALING, + ) { + Ok(fd) => { + // This is only an optimization, so ignore errors. + // F_SEAL_SRHINK = File cannot be reduced in size. + // F_SEAL_SEAL = Prevent further calls to fcntl(). + let _ = fcntl::fcntl( + fd, + fcntl::F_ADD_SEALS( + fcntl::SealFlag::F_SEAL_SHRINK | fcntl::SealFlag::F_SEAL_SEAL, + ), + ); + return Ok(fd); + } + Err(nix::errno::Errno::EINTR) => continue, + Err(nix::errno::Errno::ENOSYS) => break, + Err(errno) => return Err(std::io::Error::from(errno)), + } + } + + // Fallback to using shm_open. + let sys_time = SystemTime::now(); + let mut mem_file_handle = format!( + "/libwayshot-{}", + sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() + ); + loop { + match mman::shm_open( + // O_CREAT = Create file if does not exist. + // O_EXCL = Error if create and file exists. + // O_RDWR = Open for reading and writing. + // O_CLOEXEC = Close on succesful execution. + // S_IRUSR = Set user read permission bit . + // S_IWUSR = Set user write permission bit. + mem_file_handle.as_str(), + fcntl::OFlag::O_CREAT + | fcntl::OFlag::O_EXCL + | fcntl::OFlag::O_RDWR + | fcntl::OFlag::O_CLOEXEC, + stat::Mode::S_IRUSR | stat::Mode::S_IWUSR, + ) { + Ok(fd) => match mman::shm_unlink(mem_file_handle.as_str()) { + Ok(_) => return Ok(fd), + Err(errno) => match unistd::close(fd) { + Ok(_) => return Err(std::io::Error::from(errno)), + Err(errno) => return Err(std::io::Error::from(errno)), + }, + }, + Err(nix::errno::Errno::EEXIST) => { + // If a file with that handle exists then change the handle + mem_file_handle = format!( + "/libwayshot-{}", + sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() + ); + continue; + } + Err(nix::errno::Errno::EINTR) => continue, + Err(errno) => return Err(std::io::Error::from(errno)), + } + } +} diff --git a/wayshot/Cargo.toml b/wayshot/Cargo.toml index 13f5d8e4..2314d026 100644 --- a/wayshot/Cargo.toml +++ b/wayshot/Cargo.toml @@ -19,12 +19,9 @@ clap = "4.3.11" env_logger = { version = "0.10.0", default-features = false, features = ["auto-color", "color"] } log = "0.4.19" -wayland-client = "0.30.2" -wayland-protocols = { version = "0.30.0", features=["client", "unstable"] } - libwayshot = { version="0.1.2", path = "../libwayshot" } -image = { version = "0.24", default-features = false, features = ["jpeg", "png", "pnm"] } +image = { version = "0.24", default-features = false, features = ["jpeg", "png", "pnm", "qoi"] } dialoguer = { version = "0.10.4", features = ["fuzzy-select"] } diff --git a/wayshot/src/output.rs b/wayshot/src/output.rs deleted file mode 100644 index 7b629a75..00000000 --- a/wayshot/src/output.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::process::exit; -use wayland_client::{ - delegate_noop, - globals::GlobalList, - protocol::{wl_output, wl_output::WlOutput, wl_registry, wl_registry::WlRegistry}, - Connection, Dispatch, QueueHandle, WEnum, -}; -use wayland_protocols::xdg::xdg_output::zv1::client::{ - zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1, zxdg_output_v1::ZxdgOutputV1, -}; - -#[derive(Debug, Clone)] -pub struct OutputInfo { - pub wl_output: WlOutput, - pub name: String, - pub transform: wl_output::Transform, - pub dimensions: OutputPositioning, -} - -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct OutputPositioning { - pub x: i32, - pub y: i32, - pub width: i32, - pub height: i32, -} - -struct OutputCaptureState { - outputs: Vec, -} - -impl Dispatch for OutputCaptureState { - fn event( - state: &mut Self, - wl_registry: &WlRegistry, - event: wl_registry::Event, - _: &(), - _: &Connection, - qh: &QueueHandle, - ) { - /* > The name event is sent after binding the output object. This event - * is only sent once per output object, and the name does not change - * over the lifetime of the wl_output global. */ - - if let wl_registry::Event::Global { - name, - interface, - version, - } = event - { - if interface == "wl_output" { - if version >= 4 { - let output = wl_registry.bind::(name, 4, qh, ()); - state.outputs.push(OutputInfo { - wl_output: output, - name: "".to_string(), - transform: wl_output::Transform::Normal, - dimensions: OutputPositioning { - x: 0, - y: 0, - width: 0, - height: 0, - }, - }); - } else { - log::error!("Ignoring a wl_output with version < 4."); - } - } - } - } -} - -impl Dispatch for OutputCaptureState { - fn event( - state: &mut Self, - wl_output: &WlOutput, - event: wl_output::Event, - _: &(), - _: &Connection, - _: &QueueHandle, - ) { - let output: &mut OutputInfo = state - .outputs - .iter_mut() - .find(|x| x.wl_output == *wl_output) - .unwrap(); - - match event { - wl_output::Event::Name { name } => { - output.name = name; - } - wl_output::Event::Geometry { - transform: WEnum::Value(transform), - .. - } => { - output.transform = transform; - } - _ => (), - } - } -} - -delegate_noop!(OutputCaptureState: ignore ZxdgOutputManagerV1); - -impl Dispatch for OutputCaptureState { - fn event( - state: &mut Self, - _: &ZxdgOutputV1, - event: zxdg_output_v1::Event, - index: &usize, - _: &Connection, - _: &QueueHandle, - ) { - let output_info = state.outputs.get_mut(*index).unwrap(); - - match event { - zxdg_output_v1::Event::LogicalPosition { x, y } => { - output_info.dimensions.x = x; - output_info.dimensions.y = y; - log::debug!("Logical position event fired!"); - } - zxdg_output_v1::Event::LogicalSize { width, height } => { - output_info.dimensions.width = width; - output_info.dimensions.height = height; - log::debug!("Logical size event fired!"); - } - _ => {} - }; - } -} - -pub fn get_all_outputs(globals: &mut GlobalList, conn: &mut Connection) -> Vec { - // Connecting to wayland environment. - let mut state = OutputCaptureState { - outputs: Vec::new(), - }; - let mut event_queue = conn.new_event_queue::(); - let qh = event_queue.handle(); - - // Bind to xdg_output global. - let zxdg_output_manager = match globals.bind::(&qh, 3..=3, ()) { - Ok(x) => x, - Err(e) => { - log::error!("Failed to create ZxdgOutputManagerV1 version 3. Does your compositor implement ZxdgOutputManagerV1?"); - panic!("{:#?}", e); - } - }; - - // Fetch all outputs; when their names arrive, add them to the list - let _ = conn.display().get_registry(&qh, ()); - event_queue.roundtrip(&mut state).unwrap(); - event_queue.roundtrip(&mut state).unwrap(); - - let mut xdg_outputs: Vec = Vec::new(); - - // We loop over each output and request its position data. - for (index, output) in state.outputs.clone().iter().enumerate() { - let xdg_output = zxdg_output_manager.get_xdg_output(&output.wl_output, &qh, index); - xdg_outputs.push(xdg_output); - } - - event_queue.roundtrip(&mut state).unwrap(); - - for xdg_output in xdg_outputs { - xdg_output.destroy(); - } - - if state.outputs.is_empty() { - log::error!("Compositor did not advertise any wl_output devices!"); - exit(1); - } - log::debug!("Outputs detected: {:#?}", state.outputs); - state.outputs -} diff --git a/wayshot/src/utils.rs b/wayshot/src/utils.rs index 3c5ef86a..55fe34f8 100644 --- a/wayshot/src/utils.rs +++ b/wayshot/src/utils.rs @@ -3,7 +3,9 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -pub fn parse_geometry(g: &str) -> Option { +use libwayshot::CaptureRegion; + +pub fn parse_geometry(g: &str) -> Option { let tail = g.trim(); let x_coordinate: i32; let y_coordinate: i32; @@ -30,7 +32,7 @@ pub fn parse_geometry(g: &str) -> Option { height = tail.parse::().ok()?; } - Some(libwayshot::CaptureRegion { + Some(CaptureRegion { x_coordinate, y_coordinate, width, @@ -38,7 +40,42 @@ pub fn parse_geometry(g: &str) -> Option { }) } -pub fn get_default_file_name(extension: libwayshot::EncodingFormat) -> String { +/// Supported image encoding formats. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum EncodingFormat { + /// Jpeg / jpg encoder. + Jpg, + /// Png encoder. + Png, + /// Ppm encoder. + Ppm, + /// Qoi encoder. + Qoi, +} + +impl From for image::ImageOutputFormat { + fn from(format: EncodingFormat) -> Self { + match format { + EncodingFormat::Jpg => image::ImageFormat::Jpeg.into(), + EncodingFormat::Png => image::ImageFormat::Png.into(), + EncodingFormat::Ppm => image::ImageFormat::Pnm.into(), + EncodingFormat::Qoi => image::ImageFormat::Qoi.into(), + } + } +} + +impl From for &str { + fn from(format: EncodingFormat) -> Self { + match format { + EncodingFormat::Jpg => "jpg", + EncodingFormat::Png => "png", + EncodingFormat::Ppm => "ppm", + EncodingFormat::Qoi => "qoi", + } + } +} + +pub fn get_default_file_name(extension: EncodingFormat) -> String { let time = match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(n) => n.as_secs().to_string(), Err(_) => { diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index eb89a4ff..4a657e3b 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -1,49 +1,18 @@ use std::{ - cmp, env, + env, error::Error, io::{stdout, BufWriter, Cursor, Write}, process::exit, }; -use image::imageops::overlay; -use libwayshot::CaptureRegion; -use wayland_client::{ - globals::{registry_queue_init, GlobalListContents}, - protocol::{ - wl_output::{self, WlOutput}, - wl_registry, - }, - Connection, QueueHandle, -}; +use libwayshot::WayshotConnection; mod clap; -mod image_util; -mod output; mod utils; use dialoguer::{theme::ColorfulTheme, FuzzySelect}; -// TODO: Create a xdg-shell surface, check for the enter event, grab the output from it. - -struct WayshotState {} - -impl wayland_client::Dispatch for WayshotState { - fn event( - _: &mut WayshotState, - _: &wl_registry::WlRegistry, - _: wl_registry::Event, - _: &GlobalListContents, - _: &Connection, - _: &QueueHandle, - ) { - } -} - -struct IntersectingOutput { - output: WlOutput, - region: CaptureRegion, - transform: wl_output::Transform, -} +use crate::utils::EncodingFormat; fn select_ouput(ouputs: &[T]) -> Option where @@ -75,17 +44,17 @@ fn main() -> Result<(), Box> { log::debug!("Using custom extension: {:#?}", ext); match ext.as_str() { - "jpeg" | "jpg" => libwayshot::EncodingFormat::Jpg, - "png" => libwayshot::EncodingFormat::Png, - "ppm" => libwayshot::EncodingFormat::Ppm, - "qoi" => libwayshot::EncodingFormat::Qoi, + "jpeg" | "jpg" => EncodingFormat::Jpg, + "png" => EncodingFormat::Png, + "ppm" => EncodingFormat::Ppm, + "qoi" => EncodingFormat::Qoi, _ => { log::error!("Invalid extension provided.\nValid extensions:\n1) jpeg\n2) jpg\n3) png\n4) ppm\n5) qoi"); exit(1); } } } else { - libwayshot::EncodingFormat::Png + EncodingFormat::Png }; let mut file_is_stdout: bool = false; @@ -99,192 +68,65 @@ fn main() -> Result<(), Box> { file_path = Some(utils::get_default_file_name(extension)); } - let mut conn = Connection::connect_to_env().unwrap(); - let (mut globals, _) = registry_queue_init::(&conn).unwrap(); + let wayshot_conn = WayshotConnection::new()?; if args.get_flag("listoutputs") { - let valid_outputs = output::get_all_outputs(&mut globals, &mut conn); + let valid_outputs = wayshot_conn.get_all_outputs(); for output in valid_outputs { log::info!("{:#?}", output.name); } exit(1); } - let mut cursor_overlay: i32 = 0; + let mut cursor_overlay = false; if args.get_flag("cursor") { - cursor_overlay = 1; + cursor_overlay = true; } - let capture_area = if let Some(slurpregion) = args.get_one::("slurp") { - match utils::parse_geometry(slurpregion) { - Some(region) => (wl_output::Transform::Normal, region), - None => { - log::error!("Invalid geometry specification"); - exit(1); - } + let image_buffer = if let Some(slurp_region) = args.get_one::("slurp") { + if let Some(region) = utils::parse_geometry(slurp_region) { + wayshot_conn.screenshot(region, cursor_overlay)? + } else { + log::error!("Invalid geometry specification"); + exit(1); } } else if let Some(output_name) = args.get_one::("output") { - let outputs = output::get_all_outputs(&mut globals, &mut conn); - let mut capture_info = None; - for output in outputs { - if &output.name == output_name { - capture_info = Some(( - output.transform, - CaptureRegion { - x_coordinate: output.dimensions.x, - y_coordinate: output.dimensions.y, - width: output.dimensions.width, - height: output.dimensions.height, - }, - )) - } - } - - if capture_info.is_none() { + let outputs = wayshot_conn.get_all_outputs(); + if let Some(output) = outputs + .into_iter() + .find(|output| &output.name == output_name) + { + wayshot_conn.screenshot_outputs(vec![output], cursor_overlay)? + } else { log::error!("No output found!\n"); exit(1); } - - capture_info.unwrap() } else if args.get_flag("chooseoutput") { - let outputs = output::get_all_outputs(&mut globals, &mut conn); + let outputs = wayshot_conn.get_all_outputs(); let output_names: Vec = outputs .iter() .map(|display| display.name.to_string()) .collect(); if let Some(index) = select_ouput(&output_names) { - ( - outputs[index].transform, - CaptureRegion { - x_coordinate: outputs[index].dimensions.x, - y_coordinate: outputs[index].dimensions.y, - width: outputs[index].dimensions.width, - height: outputs[index].dimensions.height, - }, - ) + wayshot_conn.screenshot_outputs(vec![outputs[index].clone()], cursor_overlay)? } else { log::error!("No output found!\n"); exit(1); } } else { - let output = &output::get_all_outputs(&mut globals, &mut conn)[0]; - ( - output.transform, - CaptureRegion { - x_coordinate: output.dimensions.x, - y_coordinate: output.dimensions.y, - width: output.dimensions.width, - height: output.dimensions.height, - }, - ) - }; - - let frame_copy: (Vec, Option<(i32, i32)>) = { - let transform = capture_area.0; - let region = capture_area.1; - - let mut framecopys: Vec = Vec::new(); - - let outputs = output::get_all_outputs(&mut globals, &mut conn); - let mut intersecting_outputs: Vec = Vec::new(); - for output in outputs.iter() { - let x1: i32 = cmp::max(output.dimensions.x, region.x_coordinate); - let y1: i32 = cmp::max(output.dimensions.y, region.y_coordinate); - let x2: i32 = cmp::min( - output.dimensions.x + output.dimensions.width, - region.x_coordinate + region.width, - ); - let y2: i32 = cmp::min( - output.dimensions.y + output.dimensions.height, - region.y_coordinate + region.height, - ); - - let width = x2 - x1; - let height = y2 - y1; - - if !(width <= 0 || height <= 0) { - let true_x = region.x_coordinate - output.dimensions.x; - let true_y = region.y_coordinate - output.dimensions.y; - let true_region = CaptureRegion { - x_coordinate: true_x, - y_coordinate: true_y, - width: region.width, - height: region.height, - }; - intersecting_outputs.push(IntersectingOutput { - output: output.wl_output.clone(), - region: true_region, - transform, - }); - } - } - if intersecting_outputs.is_empty() { - log::error!("Provided capture region doesn't intersect with any outputs!"); - exit(1); - } - - for intersecting_output in intersecting_outputs { - framecopys.push(libwayshot::capture_output_frame( - &mut globals, - &mut conn, - cursor_overlay, - intersecting_output.output.clone(), - intersecting_output.transform, - Some(intersecting_output.region), - )?); - } - (framecopys, Some((region.width, region.height))) + wayshot_conn.screenshot_all(cursor_overlay)? }; - let mut composited_image; - let mut buffer; - - if frame_copy.0.len() == 1 { - let (width, height) = frame_copy.1.unwrap(); - let frame_copy = &frame_copy.0[0]; - - buffer = Cursor::new(Vec::new()); - libwayshot::write_to_file(&mut buffer, extension, frame_copy)?; - - let image = image::load_from_memory(buffer.get_ref())?; - composited_image = image_util::rotate_image_buffer( - &image, - frame_copy.transform, - width as u32, - height as u32, - ); - } else { - let mut images = Vec::new(); - let (frame_copy, region) = frame_copy; - let (width, height) = region.unwrap(); - for frame_copy in frame_copy { - buffer = Cursor::new(Vec::new()); - libwayshot::write_to_file(&mut buffer, extension, &frame_copy)?; - let image = image::load_from_memory(buffer.get_ref())?; - let image = image_util::rotate_image_buffer( - &image, - frame_copy.transform, - width as u32, - height as u32, - ); - images.push(image); - } - composited_image = images[0].clone(); - for image in images { - overlay(&mut composited_image, &image, 0, 0); - } - } - if file_is_stdout { let stdout = stdout(); let mut buffer = Cursor::new(Vec::new()); let mut writer = BufWriter::new(stdout.lock()); - composited_image.write_to(&mut buffer, extension)?; + image_buffer.write_to(&mut buffer, extension)?; writer.write_all(buffer.get_ref())?; } else { - composited_image.save(file_path.unwrap())?; + image_buffer.save(file_path.unwrap())?; } Ok(())