From 430296d3bc1a47150cde6b9e9e6e8ad5c85927c4 Mon Sep 17 00:00:00 2001 From: nashaofu Date: Thu, 21 Dec 2023 09:52:34 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=EF=BC=8C=E6=94=AF=E6=8C=81=E8=8E=B7=E5=8F=96window?= =?UTF-8?q?=E6=88=AA=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 34 +-- README.md | 87 ++++-- examples/monitor.rs | 33 +++ examples/monitor_capture.rs | 25 ++ examples/screenshots.rs | 27 -- examples/window.rs | 22 ++ examples/window_capture.rs | 44 ++++ src/darwin.rs | 56 ---- src/error.rs | 55 ++++ src/lib.rs | 118 +-------- src/linux/capture.rs | 57 ++++ src/linux/impl_monitor.rs | 227 ++++++++++++++++ src/linux/impl_window.rs | 221 ++++++++++++++++ src/linux/mod.rs | 51 +--- src/linux/utils.rs | 48 ++++ src/linux/wayland.rs | 28 -- ...yland_screenshot.rs => wayland_capture.rs} | 51 ++-- src/linux/{xorg.rs => xorg_capture.rs} | 57 ++-- src/macos/capture.rs | 31 +++ src/macos/impl_monitor.rs | 128 +++++++++ src/macos/impl_window.rs | 182 +++++++++++++ src/macos/mod.rs | 4 + src/monitor.rs | 81 ++++++ src/{image_utils.rs => utils/image.rs} | 28 +- src/utils/mod.rs | 1 + src/win32.rs | 247 ------------------ src/window.rs | 74 ++++++ src/windows/boxed.rs | 84 ++++++ src/windows/capture.rs | 126 +++++++++ src/windows/impl_monitor.rs | 166 ++++++++++++ src/windows/impl_window.rs | 168 ++++++++++++ src/windows/mod.rs | 6 + src/windows/utils.rs | 10 + 33 files changed, 1946 insertions(+), 631 deletions(-) create mode 100644 examples/monitor.rs create mode 100644 examples/monitor_capture.rs delete mode 100644 examples/screenshots.rs create mode 100644 examples/window.rs create mode 100644 examples/window_capture.rs delete mode 100644 src/darwin.rs create mode 100644 src/error.rs create mode 100644 src/linux/capture.rs create mode 100644 src/linux/impl_monitor.rs create mode 100644 src/linux/impl_window.rs create mode 100644 src/linux/utils.rs delete mode 100644 src/linux/wayland.rs rename src/linux/{wayland_screenshot.rs => wayland_capture.rs} (78%) rename src/linux/{xorg.rs => xorg_capture.rs} (66%) create mode 100644 src/macos/capture.rs create mode 100644 src/macos/impl_monitor.rs create mode 100644 src/macos/impl_window.rs create mode 100644 src/macos/mod.rs create mode 100644 src/monitor.rs rename src/{image_utils.rs => utils/image.rs} (72%) create mode 100644 src/utils/mod.rs delete mode 100644 src/win32.rs create mode 100644 src/window.rs create mode 100644 src/windows/boxed.rs create mode 100644 src/windows/capture.rs create mode 100644 src/windows/impl_monitor.rs create mode 100644 src/windows/impl_window.rs create mode 100644 src/windows/mod.rs create mode 100644 src/windows/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 0758be9..9991c09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,32 +1,34 @@ [package] -name = "screenshots" -version = "0.8.7" +name = "xcap" +version = "0.0.1" edition = "2021" -description = "A cross-platform screen capturer library" +description = "A cross-platform screen capture library" license = "Apache-2.0" -documentation = "https://docs.rs/screenshots" -homepage = "https://github.com/nashaofu/screenshots-rs" -repository = "https://github.com/nashaofu/screenshots-rs.git" -keywords = ["screenshots", "screenshot", "screen", "capture"] +documentation = "https://docs.rs/xcap" +homepage = "https://github.com/nashaofu/xcap" +repository = "https://github.com/nashaofu/xcap.git" +keywords = ["screen", "monitor", "window", "capture", "image"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0" -display-info = "0.4" image = "0.24" -percent-encoding = "2.3" +thiserror = "1.0" [target.'cfg(target_os = "macos")'.dependencies] -core-graphics = "0.22" +core-foundation = "0.9" +core-graphics = "0.23" [target.'cfg(target_os = "windows")'.dependencies] -fxhash = "0.2" -widestring = "1.0" -windows = { version = "0.51", features = [ +windows = { version = "0.52", features = [ "Win32_Foundation", "Win32_Graphics_Gdi", + "Win32_Graphics_Dwm", + "Win32_UI_WindowsAndMessaging", + "Win32_Storage_Xps", ] } [target.'cfg(target_os="linux")'.dependencies] +percent-encoding = "2.3" +xcb = { version = "1.3", features = ["randr"] } dbus = { version = "0.9", features = ["vendored"] } -libwayshot = "0.2" -xcb = "1.2" diff --git a/README.md b/README.md index b845ec9..70951a1 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,90 @@ -# 📷 Screenshots +# 📷 XCap -Screenshots is a cross-platform screenshots library for MacOS, Windows, Linux (X11, Wayland) written in Rust. It provides a simple API for capturing screenshots of a screen or a specific area of a screen. +XCap is a cross-platform screen capture library for MacOS, Windows, Linux (X11, Wayland) written in Rust. It provides a simple API for capturing screen capture of a screen or a specific area of a screen. + +## Features + +- Cross-platform support: Windows Mac and Linux. +- Multiple capture modes: screen window. +- Video capture、audio capture soon. ## Example -The following example shows how to capture screenshots of all screens and a specific area of a screen. +- Monitor capture ```rust -use screenshots::Screen; +use xcap::Monitor; use std::time::Instant; +fn normalized(filename: &str) -> String { + filename + .replace("|", "") + .replace("\\", "") + .replace(":", "") + .replace("/", "") +} + fn main() { let start = Instant::now(); - let screens = Screen::all().unwrap(); + let monitors = Monitor::all().unwrap(); + + for monitor in monitors { + let image = monitor.capture_image().unwrap(); - for screen in screens { - println!("capturer {screen:?}"); - let mut image = screen.capture().unwrap(); image - .save(format!("target/{}.png", screen.display_info.id)) + .save(format!("target/monitor-{}.png", normalized(monitor.name()))) .unwrap(); + } + + println!("运行耗时: {:?}", start.elapsed()); +} +``` + +- Window capture + +```rust +use xcap::Window; +use std::time::Instant; + +fn normalized(filename: &str) -> String { + filename + .replace("|", "") + .replace("\\", "") + .replace(":", "") + .replace("/", "") +} + +fn main() { + let start = Instant::now(); + let windows = Window::all().unwrap(); + + let mut i = 0; + + for window in windows { + // 最小化的窗口不能截屏 + if window.is_minimized() { + continue; + } - image = screen.capture_area(300, 300, 300, 300).unwrap(); + println!( + "Window: {:?} {:?} {:?}", + window.title(), + (window.x(), window.y(), window.width(), window.height()), + (window.is_minimized(), window.is_maximized()) + ); + + let image = window.capture_image().unwrap(); image - .save(format!("target/{}-2.png", screen.display_info.id)) + .save(format!( + "target/window-{}-{}.png", + i, + normalized(window.title()) + )) .unwrap(); - } - let screen = Screen::from_point(100, 100).unwrap(); - println!("capturer {screen:?}"); + i += 1; + } - let image = screen.capture_area(300, 300, 300, 300).unwrap(); - image.save("target/capture_display_with_point.png").unwrap(); println!("运行耗时: {:?}", start.elapsed()); } ``` @@ -60,4 +113,4 @@ pacman -S libxcb libxrandr dbus ## License -This project is licensed under the Apache License. See the [LICENSE](LICENSE) file for details. +This project is licensed under the Apache License. See the [LICENSE](../LICENSE) file for details. diff --git a/examples/monitor.rs b/examples/monitor.rs new file mode 100644 index 0000000..2939691 --- /dev/null +++ b/examples/monitor.rs @@ -0,0 +1,33 @@ +use std::time::Instant; +use xcap::Monitor; + +fn main() { + let start = Instant::now(); + let monitors = Monitor::all().unwrap(); + println!("Monitor::all() 运行耗时: {:?}", start.elapsed()); + + for monitor in monitors { + println!( + "Monitor: {} {} {:?} {:?}", + monitor.id(), + monitor.name(), + (monitor.x(), monitor.y(), monitor.width(), monitor.height()), + ( + monitor.rotation(), + monitor.scale_factor(), + monitor.frequency(), + monitor.is_primary() + ) + ); + } + + let monitor = Monitor::from_point(100, 100).unwrap(); + + println!("Monitor::from_point(): {}", monitor.name()); + println!( + "Monitor::from_point(100, 100) 运行耗时: {:?}", + start.elapsed() + ); + + println!("运行耗时: {:?}", start.elapsed()); +} diff --git a/examples/monitor_capture.rs b/examples/monitor_capture.rs new file mode 100644 index 0000000..248f20f --- /dev/null +++ b/examples/monitor_capture.rs @@ -0,0 +1,25 @@ +use std::time::Instant; +use xcap::Monitor; + +fn normalized(filename: &str) -> String { + filename + .replace("|", "") + .replace("\\", "") + .replace(":", "") + .replace("/", "") +} + +fn main() { + let start = Instant::now(); + let monitors = Monitor::all().unwrap(); + + for monitor in monitors { + let image = monitor.capture_image().unwrap(); + + image + .save(format!("target/monitor-{}.png", normalized(monitor.name()))) + .unwrap(); + } + + println!("运行耗时: {:?}", start.elapsed()); +} diff --git a/examples/screenshots.rs b/examples/screenshots.rs deleted file mode 100644 index 2b35f50..0000000 --- a/examples/screenshots.rs +++ /dev/null @@ -1,27 +0,0 @@ -use screenshots::Screen; -use std::time::Instant; - -fn main() { - let start = Instant::now(); - let screens = Screen::all().unwrap(); - - for screen in screens { - println!("capturer {screen:?}"); - let mut image = screen.capture().unwrap(); - image - .save(format!("target/{}.png", screen.display_info.id)) - .unwrap(); - - image = screen.capture_area(300, 300, 300, 300).unwrap(); - image - .save(format!("target/{}-2.png", screen.display_info.id)) - .unwrap(); - } - - let screen = Screen::from_point(100, 100).unwrap(); - println!("capturer {screen:?}"); - - let image = screen.capture_area(300, 300, 300, 300).unwrap(); - image.save("target/capture_display_with_point.png").unwrap(); - println!("运行耗时: {:?}", start.elapsed()); -} diff --git a/examples/window.rs b/examples/window.rs new file mode 100644 index 0000000..33ef299 --- /dev/null +++ b/examples/window.rs @@ -0,0 +1,22 @@ +use std::time::Instant; +use xcap::Window; + +fn main() { + let start = Instant::now(); + let windows = Window::all().unwrap(); + println!("Window::all() 运行耗时: {:?}", start.elapsed()); + + for window in windows { + println!( + "Window: {} {} {} {:?} {:?} {:?}", + window.id(), + window.title(), + window.app_name(), + window.current_monitor().name(), + (window.x(), window.y(), window.width(), window.height()), + (window.is_minimized(), window.is_maximized()) + ); + } + + println!("运行耗时: {:?}", start.elapsed()); +} diff --git a/examples/window_capture.rs b/examples/window_capture.rs new file mode 100644 index 0000000..d887e5d --- /dev/null +++ b/examples/window_capture.rs @@ -0,0 +1,44 @@ +use std::time::Instant; +use xcap::Window; + +fn normalized(filename: &str) -> String { + filename + .replace("|", "") + .replace("\\", "") + .replace(":", "") + .replace("/", "") +} + +fn main() { + let start = Instant::now(); + let windows = Window::all().unwrap(); + + let mut i = 0; + + for window in windows { + // 最小化的窗口不能截屏 + if window.is_minimized() { + continue; + } + + println!( + "Window: {:?} {:?} {:?}", + window.title(), + (window.x(), window.y(), window.width(), window.height()), + (window.is_minimized(), window.is_maximized()) + ); + + let image = window.capture_image().unwrap(); + image + .save(format!( + "target/window-{}-{}.png", + i, + normalized(window.title()) + )) + .unwrap(); + + i += 1; + } + + println!("运行耗时: {:?}", start.elapsed()); +} diff --git a/src/darwin.rs b/src/darwin.rs deleted file mode 100644 index be16623..0000000 --- a/src/darwin.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::image_utils::{bgra_to_rgba_image, remove_extra_data}; -use anyhow::{anyhow, Result}; -use core_graphics::{ - display::{kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay}, - geometry::{CGPoint, CGRect, CGSize}, -}; -use display_info::DisplayInfo; -use image::RgbaImage; - -fn capture(display_info: &DisplayInfo, cg_rect: CGRect) -> Result { - let cg_image = CGDisplay::screenshot( - cg_rect, - kCGWindowListOptionOnScreenOnly, - kCGNullWindowID, - kCGWindowImageDefault, - ) - .ok_or_else(|| anyhow!("Screen:{} screenshot failed", display_info.id))?; - - let width = cg_image.width(); - let height = cg_image.height(); - let clean_buf = remove_extra_data( - width, - height, - cg_image.bytes_per_row(), - Vec::from(cg_image.data().bytes()), - ); - - bgra_to_rgba_image(width as u32, height as u32, clean_buf) -} - -pub fn capture_screen(display_info: &DisplayInfo) -> Result { - let cg_display = CGDisplay::new(display_info.id); - capture(display_info, cg_display.bounds()) -} - -pub fn capture_screen_area( - display_info: &DisplayInfo, - x: i32, - y: i32, - width: u32, - height: u32, -) -> Result { - let cg_display = CGDisplay::new(display_info.id); - let mut cg_rect = cg_display.bounds(); - let origin = cg_rect.origin; - - let rect_x = origin.x + (x as f64); - let rect_y = origin.y + (y as f64); - let rect_width = width as f64; - let rect_height = height as f64; - - cg_rect.origin = CGPoint::new(rect_x, rect_y); - cg_rect.size = CGSize::new(rect_width, rect_height); - - capture(display_info, cg_rect) -} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..e93880c --- /dev/null +++ b/src/error.rs @@ -0,0 +1,55 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum XCapError { + #[error("{0}")] + Error(String), + + #[cfg(target_os = "linux")] + #[error(transparent)] + XcbError(#[from] xcb::Error), + #[cfg(target_os = "linux")] + #[error(transparent)] + XcbConnError(#[from] xcb::ConnError), + #[cfg(target_os = "linux")] + #[error(transparent)] + ImageImageError(#[from] image::ImageError), + #[cfg(target_os = "linux")] + #[error(transparent)] + StdStrUtf8Error(#[from] std::str::Utf8Error), + #[cfg(target_os = "linux")] + #[error(transparent)] + DbusError(#[from] dbus::Error), + #[cfg(target_os = "linux")] + #[error(transparent)] + StdIOError(#[from] std::io::Error), + #[cfg(target_os = "linux")] + #[error(transparent)] + StdTimeSystemTimeError(#[from] std::time::SystemTimeError), + + #[cfg(target_os = "macos")] + #[error("CoreGraphicsDisplayCGError {0}")] + CoreGraphicsDisplayCGError(core_graphics::display::CGError), + + #[cfg(target_os = "windows")] + #[error(transparent)] + WindowsCoreError(#[from] windows::core::Error), + #[cfg(target_os = "windows")] + #[error(transparent)] + StdStringFromUtf16Error(#[from] std::string::FromUtf16Error), +} + +impl XCapError { + pub fn new(err: S) -> Self { + XCapError::Error(err.to_string()) + } +} + +#[cfg(target_os = "macos")] +impl From for XCapError { + fn from(value: core_graphics::display::CGError) -> Self { + XCapError::CoreGraphicsDisplayCGError(value) + } +} + +pub type XCapResult = Result; diff --git a/src/lib.rs b/src/lib.rs index dce3f81..8feda74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,112 +1,22 @@ -mod image_utils; +mod error; +mod monitor; +mod utils; +mod window; -use anyhow::{anyhow, Result}; -use display_info::DisplayInfo; -use image::RgbaImage; - -pub use display_info; -pub use image; - -#[cfg(target_os = "macos")] -mod darwin; #[cfg(target_os = "macos")] -use darwin::*; +#[path = "macos/mod.rs"] +mod platform; #[cfg(target_os = "windows")] -mod win32; -#[cfg(target_os = "windows")] -use win32::*; +#[path = "windows/mod.rs"] +mod platform; #[cfg(target_os = "linux")] -mod linux; -#[cfg(target_os = "linux")] -use linux::*; - -/// This struct represents a screen capturer. -#[derive(Debug, Clone, Copy)] -pub struct Screen { - pub display_info: DisplayInfo, -} - -impl Screen { - /// Get a screen from the [display_info]. - /// - /// [display_info]: https://docs.rs/display-info/latest/display_info/struct.DisplayInfo.html - pub fn new(display_info: &DisplayInfo) -> Self { - Screen { - display_info: *display_info, - } - } +#[path = "linux/mod.rs"] +mod platform; - /// Return all available screens. - pub fn all() -> Result> { - let screens = DisplayInfo::all()?.iter().map(Screen::new).collect(); - Ok(screens) - } - - /// Get a screen which includes the point with the given coordinates. - pub fn from_point(x: i32, y: i32) -> Result { - let display_info = DisplayInfo::from_point(x, y)?; - Ok(Screen::new(&display_info)) - } - - /// Capture a screenshot of the screen. - pub fn capture(&self) -> Result { - capture_screen(&self.display_info) - } - - /// Captures a screenshot of the designated area of the screen. - pub fn capture_area(&self, x: i32, y: i32, width: u32, height: u32) -> Result { - let display_info = self.display_info; - let screen_x2 = display_info.x + display_info.width as i32; - let screen_y2 = display_info.y + display_info.height as i32; - - // Use clamp to ensure x1 and y1 are within the screen bounds - let x1 = (x + display_info.x).clamp(display_info.x, screen_x2); - let y1 = (y + display_info.y).clamp(display_info.y, screen_y2); - - // Calculate x2 and y2 and use min to ensure they do not exceed the screen bounds - let x2 = std::cmp::min(x1 + width as i32, screen_x2); - let y2 = std::cmp::min(y1 + height as i32, screen_y2); - - // Check if the area size is valid - if x1 >= x2 || y1 >= y2 { - return Err(anyhow!("Area size is invalid")); - } - - // Capture the screen area - capture_screen_area( - &display_info, - x1 - display_info.x, - y1 - display_info.y, - (x2 - x1) as u32, - (y2 - y1) as u32, - ) - } +pub use image; - #[cfg(target_os = "windows")] - /// No capture area check, caller is responsible for calculating - /// the correct parameters according to Screen::display_info. - /// Example: - /// ``` - /// use screenshots::Screen; - /// - /// for screen in Screen::all().unwrap() { - /// println!("Capturing screen info: {screen:?}"); - /// let scale = screen.display_info.scale_factor; - /// let real_resoltion = ((screen.display_info.width as f64 * scale as f64) as u32, (screen.display_info.height as f64 * scale as f64) as u32); - /// let image = screen.capture_area_ignore_area_check(0, 0, real_resoltion.0, real_resoltion.1).unwrap(); - /// image.save(&format!("screenshot_screen_{}.png", screen.display_info.id)).unwrap(); - /// } - /// ``` - pub fn capture_area_ignore_area_check( - &self, - x: i32, - y: i32, - width: u32, - height: u32, - ) -> Result { - let display_info = self.display_info; - capture_screen_area_ignore_sf(&display_info, x, y, width, height) - } -} +pub use error::{XCapError, XCapResult}; +pub use monitor::Monitor; +pub use window::Window; diff --git a/src/linux/capture.rs b/src/linux/capture.rs new file mode 100644 index 0000000..4cff6e3 --- /dev/null +++ b/src/linux/capture.rs @@ -0,0 +1,57 @@ +use image::RgbaImage; +use std::env::var_os; + +use crate::error::XCapResult; + +use super::{ + impl_monitor::ImplMonitor, impl_window::ImplWindow, wayland_capture::wayland_capture, + xorg_capture::xorg_capture, +}; + +fn wayland_detect() -> bool { + let xdg_session_type = var_os("XDG_SESSION_TYPE") + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + let wayland_display = var_os("WAYLAND_DISPLAY") + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + xdg_session_type.eq("wayland") || wayland_display.to_lowercase().contains("wayland") +} + +pub fn capture_monitor(impl_monitor: &ImplMonitor) -> XCapResult { + if wayland_detect() { + wayland_capture(impl_monitor) + } else { + let x = ((impl_monitor.x as f32) * impl_monitor.scale_factor) as i32; + let y = ((impl_monitor.y as f32) * impl_monitor.scale_factor) as i32; + let width = ((impl_monitor.width as f32) * impl_monitor.scale_factor) as u32; + let height = ((impl_monitor.height as f32) * impl_monitor.scale_factor) as u32; + + xorg_capture(impl_monitor.screen_buf.root(), x, y, width, height) + } +} + +pub fn capture_window(impl_window: &ImplWindow) -> XCapResult { + let width = impl_window.width; + let height = impl_window.height; + + xorg_capture(impl_window.window, 0, 0, width, height) +} + +// fn capture_screen_area( +// screen_info: &ScreenInfo, +// x: i32, +// y: i32, +// width: u32, +// height: u32, +// ) -> XCapResult { +// if wayland_detect() { +// wayland_capture_screen_area(screen_info, x, y, width, height) +// } else { +// xorg_capture_screen_area(screen_info, x, y, width, height) +// } +// } diff --git a/src/linux/impl_monitor.rs b/src/linux/impl_monitor.rs new file mode 100644 index 0000000..4abd98c --- /dev/null +++ b/src/linux/impl_monitor.rs @@ -0,0 +1,227 @@ +use image::RgbaImage; +use std::str; +use xcb::{ + randr::{ + GetCrtcInfo, GetMonitors, GetOutputInfo, GetScreenResources, Mode, ModeFlag, ModeInfo, + MonitorInfo, MonitorInfoBuf, Output, Rotation, + }, + x::{GetProperty, Screen, ScreenBuf, ATOM_RESOURCE_MANAGER, ATOM_STRING, CURRENT_TIME}, + Connection, Xid, +}; + +use crate::error::{XCapError, XCapResult}; + +use super::capture::capture_monitor; + +#[derive(Debug, Clone)] +pub(crate) struct ImplMonitor { + pub screen_buf: ScreenBuf, + #[allow(unused)] + pub monitor_info_buf: MonitorInfoBuf, + pub id: u32, + pub name: String, + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, + pub rotation: f32, + pub scale_factor: f32, + pub frequency: f32, + pub is_primary: bool, +} + +// per https://gitlab.freedesktop.org/xorg/app/xrandr/-/blob/master/xrandr.c#L576 +fn get_current_frequency(mode_infos: &[ModeInfo], mode: Mode) -> f32 { + let mode_info = match mode_infos.iter().find(|m| m.id == mode.resource_id()) { + Some(mode_info) => mode_info, + None => return 0.0, + }; + + let vtotal = { + let mut val = mode_info.vtotal; + if mode_info.mode_flags.contains(ModeFlag::DOUBLE_SCAN) { + val *= 2; + } + if mode_info.mode_flags.contains(ModeFlag::INTERLACE) { + val /= 2; + } + val + }; + + if vtotal != 0 && mode_info.htotal != 0 { + (mode_info.dot_clock as f32) / (vtotal as f32 * mode_info.htotal as f32) + } else { + 0.0 + } +} + +fn get_scale_factor(conn: &Connection, screen: &Screen) -> XCapResult { + let xft_dpi_prefix = "Xft.dpi:\t"; + + let get_property_cookie = conn.send_request(&GetProperty { + delete: false, + window: screen.root(), + property: ATOM_RESOURCE_MANAGER, + r#type: ATOM_STRING, + long_offset: 0, + long_length: 60, + }); + + let get_property_reply = conn.wait_for_reply(get_property_cookie)?; + + let resource_manager = str::from_utf8(get_property_reply.value())?; + + let xft_dpi = resource_manager + .split('\n') + .find(|s| s.starts_with(xft_dpi_prefix)) + .ok_or_else(|| XCapError::new("Xft.dpi parse failed"))? + .strip_prefix(xft_dpi_prefix) + .ok_or_else(|| XCapError::new("Xft.dpi parse failed"))?; + + let dpi = xft_dpi.parse::().map_err(|err| XCapError::new(err))?; + + Ok(dpi / 96.0) +} + +fn get_rotation_frequency( + conn: &Connection, + mode_infos: &[ModeInfo], + output: &Output, +) -> XCapResult<(f32, f32)> { + let get_output_info_cookie = conn.send_request(&GetOutputInfo { + output: *output, + config_timestamp: 0, + }); + + let get_output_info_reply = conn.wait_for_reply(get_output_info_cookie)?; + + let get_crtc_info_cookie = conn.send_request(&GetCrtcInfo { + crtc: get_output_info_reply.crtc(), + config_timestamp: 0, + }); + + let get_crtc_info_reply = conn.wait_for_reply(get_crtc_info_cookie)?; + + let mode = get_crtc_info_reply.mode(); + + let rotation = match get_crtc_info_reply.rotation() { + Rotation::ROTATE_0 => 0.0, + Rotation::ROTATE_90 => 90.0, + Rotation::ROTATE_180 => 180.0, + Rotation::ROTATE_270 => 270.0, + _ => 0.0, + }; + + let frequency = get_current_frequency(mode_infos, mode); + + Ok((rotation, frequency)) +} + +impl ImplMonitor { + fn new( + conn: &Connection, + screen: &Screen, + monitor_info: &MonitorInfo, + output: &Output, + rotation: f32, + scale_factor: f32, + frequency: f32, + ) -> XCapResult { + let get_output_info_cookie = conn.send_request(&GetOutputInfo { + output: *output, + config_timestamp: CURRENT_TIME, + }); + let get_output_info_reply = conn.wait_for_reply(get_output_info_cookie)?; + + Ok(ImplMonitor { + screen_buf: screen.to_owned(), + monitor_info_buf: monitor_info.to_owned(), + id: output.resource_id(), + name: str::from_utf8(get_output_info_reply.name())?.to_string(), + x: ((monitor_info.x() as f32) / scale_factor) as i32, + y: ((monitor_info.y() as f32) / scale_factor) as i32, + width: ((monitor_info.width() as f32) / scale_factor) as u32, + height: ((monitor_info.height() as f32) / scale_factor) as u32, + rotation, + scale_factor, + frequency, + is_primary: monitor_info.primary(), + }) + } + + pub fn all() -> XCapResult> { + let (conn, index) = Connection::connect(None)?; + + let setup = conn.get_setup(); + + let screen = setup + .roots() + .nth(index as usize) + .ok_or_else(|| XCapError::new("Not found screen"))?; + + let scale_factor = get_scale_factor(&conn, screen).unwrap_or(1.0); + + let get_monitors_cookie = conn.send_request(&GetMonitors { + window: screen.root(), + get_active: true, + }); + + let get_monitors_reply = conn.wait_for_reply(get_monitors_cookie)?; + + let monitor_info_iterator = get_monitors_reply.monitors(); + + let get_screen_resources_cookie = conn.send_request(&GetScreenResources { + window: screen.root(), + }); + + let get_screen_resources_reply = conn.wait_for_reply(get_screen_resources_cookie)?; + + let mode_infos = get_screen_resources_reply.modes(); + + let mut impl_monitor = Vec::new(); + + for monitor_info in monitor_info_iterator { + let output = monitor_info + .outputs() + .get(0) + .ok_or_else(|| XCapError::new("Not found output"))?; + + let (rotation, frequency) = + get_rotation_frequency(&conn, mode_infos, output).unwrap_or((0.0, 0.0)); + + impl_monitor.push(ImplMonitor::new( + &conn, + screen, + monitor_info, + output, + rotation, + scale_factor, + frequency, + )?); + } + + Ok(impl_monitor) + } + + pub fn from_point(x: i32, y: i32) -> XCapResult { + let impl_monitors = ImplMonitor::all()?; + + let impl_monitor = impl_monitors + .iter() + .find(|impl_monitor| { + x >= impl_monitor.x + && x < impl_monitor.x + impl_monitor.width as i32 + && y >= impl_monitor.y + && y < impl_monitor.y + impl_monitor.height as i32 + }) + .ok_or_else(|| XCapError::new("Get screen info failed"))?; + + Ok(impl_monitor.clone()) + } +} + +impl ImplMonitor { + pub fn capture_image(&self) -> XCapResult { + capture_monitor(self) + } +} diff --git a/src/linux/impl_window.rs b/src/linux/impl_window.rs new file mode 100644 index 0000000..6662fbe --- /dev/null +++ b/src/linux/impl_window.rs @@ -0,0 +1,221 @@ +use image::RgbaImage; +use std::str; +use xcb::{ + x::{ + Atom, Drawable, GetGeometry, GetProperty, GetPropertyReply, InternAtom, QueryPointer, + TranslateCoordinates, Window, ATOM_ATOM, ATOM_NONE, ATOM_STRING, ATOM_WM_CLASS, + ATOM_WM_NAME, + }, + Connection, Xid, +}; + +use crate::error::{XCapError, XCapResult}; + +use super::{capture::capture_window, impl_monitor::ImplMonitor, utils::Rect}; + +#[derive(Debug, Clone)] +pub(crate) struct ImplWindow { + pub window: Window, + pub id: u32, + pub title: String, + pub app_name: String, + pub current_monitor: ImplMonitor, + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, + pub is_minimized: bool, + pub is_maximized: bool, +} + +fn get_atom(conn: &Connection, name: &str) -> XCapResult { + let atom_cookie = conn.send_request(&InternAtom { + only_if_exists: true, + name: name.as_bytes(), + }); + let atom_reply = conn.wait_for_reply(atom_cookie)?; + let atom = atom_reply.atom(); + + if atom == ATOM_NONE { + return Err(XCapError::new(format!("{} not supported", name))); + } + + Ok(atom) +} + +fn get_window_property( + conn: &Connection, + window: Window, + property: Atom, + r#type: Atom, + long_offset: u32, + long_length: u32, +) -> XCapResult { + let window_property_cookie = conn.send_request(&GetProperty { + delete: false, + window, + property, + r#type, + long_offset, + long_length, + }); + + let window_property_reply = conn.wait_for_reply(window_property_cookie)?; + + Ok(window_property_reply) +} + +impl ImplWindow { + fn new( + conn: &Connection, + window: &Window, + impl_monitors: &Vec, + ) -> XCapResult { + let title = { + let get_title_reply = + get_window_property(conn, *window, ATOM_WM_NAME, ATOM_STRING, 0, 1024)?; + str::from_utf8(get_title_reply.value())?.to_string() + }; + + let app_name = { + let get_class_reply = + get_window_property(conn, *window, ATOM_WM_CLASS, ATOM_STRING, 0, 1024)?; + + let class = str::from_utf8(get_class_reply.value())?; + + class + .split('\u{0}') + .find(|str| !str.is_empty()) + .unwrap_or("") + .to_string() + }; + + let (x, y, width, height) = { + let get_geometry_cookie = conn.send_request(&GetGeometry { + drawable: Drawable::Window(*window), + }); + let get_geometry_reply = conn.wait_for_reply(get_geometry_cookie)?; + + let translate_coordinates_cookie = conn.send_request(&TranslateCoordinates { + dst_window: get_geometry_reply.root(), + src_window: *window, + src_x: get_geometry_reply.x(), + src_y: get_geometry_reply.y(), + }); + let translate_coordinates_reply = conn.wait_for_reply(translate_coordinates_cookie)?; + + ( + (translate_coordinates_reply.dst_x() - get_geometry_reply.x()) as i32, + (translate_coordinates_reply.dst_y() - get_geometry_reply.y()) as i32, + get_geometry_reply.width() as u32, + get_geometry_reply.height() as u32, + ) + }; + + let current_monitor = { + let mut max_area = 0; + let mut find_result = impl_monitors + .get(0) + .ok_or(XCapError::new("Get screen info failed"))?; + + let window_rect = Rect::new(x, y, width, height); + + // window与哪一个monitor交集最大就属于那个monitor + for impl_monitor in impl_monitors { + let monitor_rect = Rect::new( + impl_monitor.x, + impl_monitor.y, + impl_monitor.width, + impl_monitor.height, + ); + + // 获取最大的面积 + let area = window_rect.overlap_area(monitor_rect); + if area > max_area { + max_area = area; + find_result = impl_monitor; + } + } + + find_result.to_owned() + }; + + let (is_minimized, is_maximized) = { + // https://specifications.freedesktop.org/wm-spec/1.3/ar01s05.html + let wm_state_atom = get_atom(conn, "_NET_WM_STATE")?; + let wm_state_hidden_atom = get_atom(conn, "_NET_WM_STATE_HIDDEN")?; + let wm_state_maximized_vert_atom = get_atom(conn, "_NET_WM_STATE_MAXIMIZED_VERT")?; + let wm_state_maximized_horz_atom = get_atom(conn, "_NET_WM_STATE_MAXIMIZED_HORZ")?; + + let wm_state_reply = + get_window_property(conn, *window, wm_state_atom, ATOM_ATOM, 0, 12)?; + let wm_state = wm_state_reply.value::(); + + let is_minimized = wm_state.iter().any(|&state| state == wm_state_hidden_atom); + + let is_maximized_vert = wm_state + .iter() + .any(|&state| state == wm_state_maximized_vert_atom); + + let is_maximized_horz = wm_state + .iter() + .any(|&state| state == wm_state_maximized_horz_atom); + + ( + is_minimized, + !is_minimized && is_maximized_vert && is_maximized_horz, + ) + }; + + Ok(ImplWindow { + window: *window, + id: window.resource_id(), + title, + app_name, + current_monitor, + x, + y, + width, + height, + is_minimized, + is_maximized, + }) + } + + pub fn all() -> XCapResult> { + let (conn, _) = Connection::connect(None)?; + let setup = conn.get_setup(); + + // https://github.com/rust-x-bindings/rust-xcb/blob/main/examples/get_all_windows.rs + let client_list_atom = get_atom(&conn, "_NET_CLIENT_LIST")?; + + let mut impl_windows = Vec::new(); + let impl_monitors = ImplMonitor::all()?; + + for screen in setup.roots() { + let root_window = screen.root(); + + let query_pointer_cookie = conn.send_request(&QueryPointer { + window: root_window, + }); + let query_pointer_reply = conn.wait_for_reply(query_pointer_cookie)?; + + if query_pointer_reply.same_screen() { + let list_window_reply = + get_window_property(&conn, root_window, client_list_atom, ATOM_NONE, 0, 100)?; + + for client in list_window_reply.value::() { + impl_windows.push(ImplWindow::new(&conn, client, &impl_monitors)?); + } + } + } + + Ok(impl_windows) + } +} + +impl ImplWindow { + pub fn capture_image(&self) -> XCapResult { + capture_window(&self) + } +} diff --git a/src/linux/mod.rs b/src/linux/mod.rs index 67c471e..e28c939 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -1,46 +1,7 @@ -mod wayland; -mod wayland_screenshot; -mod xorg; +mod capture; +mod utils; +mod wayland_capture; +mod xorg_capture; -use anyhow::Result; -use display_info::DisplayInfo; -use image::RgbaImage; -use std::env::var_os; -use wayland::{wayland_capture_screen, wayland_capture_screen_area}; -use xorg::{xorg_capture_screen, xorg_capture_screen_area}; - -fn wayland_detect() -> bool { - let xdg_session_type = var_os("XDG_SESSION_TYPE") - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - let wayland_display = var_os("WAYLAND_DISPLAY") - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - xdg_session_type.eq("wayland") || wayland_display.to_lowercase().contains("wayland") -} - -pub fn capture_screen(display_info: &DisplayInfo) -> Result { - if wayland_detect() { - wayland_capture_screen(display_info) - } else { - xorg_capture_screen(display_info) - } -} - -pub fn capture_screen_area( - display_info: &DisplayInfo, - x: i32, - y: i32, - width: u32, - height: u32, -) -> Result { - if wayland_detect() { - wayland_capture_screen_area(display_info, x, y, width, height) - } else { - xorg_capture_screen_area(display_info, x, y, width, height) - } -} +pub mod impl_monitor; +pub mod impl_window; diff --git a/src/linux/utils.rs b/src/linux/utils.rs new file mode 100644 index 0000000..cf1c321 --- /dev/null +++ b/src/linux/utils.rs @@ -0,0 +1,48 @@ +use image::{open, RgbaImage}; + +use crate::error::XCapResult; + +pub(super) struct Rect { + x: i32, + y: i32, + width: u32, + height: u32, +} + +impl Rect { + // 计算两个矩形的交集面积 + pub(super) fn new(x: i32, y: i32, width: u32, height: u32) -> Rect { + Rect { + x, + y, + width, + height, + } + } + + // 计算两个矩形的交集面积 + pub(super) fn overlap_area(&self, other_rect: Rect) -> i32 { + let left = self.x.max(other_rect.x); + let top = self.y.max(other_rect.y); + let right = (self.x + self.width as i32).min(other_rect.x + other_rect.width as i32); + let bottom = (self.y + self.height as i32).min(other_rect.y + other_rect.height as i32); + + // 与0比较,如果小于0则表示两个矩形无交集 + let width = (right - left).max(0); + let height = (bottom - top).max(0); + + width * height + } +} + +pub(super) fn png_to_rgba_image( + filename: &String, + x: i32, + y: i32, + width: i32, + height: i32, +) -> XCapResult { + let mut dynamic_image = open(filename)?; + dynamic_image = dynamic_image.crop(x as u32, y as u32, width as u32, height as u32); + Ok(dynamic_image.to_rgba8()) +} diff --git a/src/linux/wayland.rs b/src/linux/wayland.rs deleted file mode 100644 index c056a4b..0000000 --- a/src/linux/wayland.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::linux::wayland_screenshot::wayland_screenshot; -use anyhow::Result; -use display_info::DisplayInfo; -use image::RgbaImage; - -pub fn wayland_capture_screen(display_info: &DisplayInfo) -> Result { - let x = ((display_info.x as f32) * display_info.scale_factor) as i32; - let y = ((display_info.y as f32) * display_info.scale_factor) as i32; - let width = (display_info.width as f32) * display_info.scale_factor; - let height = (display_info.height as f32) * display_info.scale_factor; - - wayland_screenshot(x, y, width as i32, height as i32) -} - -pub fn wayland_capture_screen_area( - display_info: &DisplayInfo, - x: i32, - y: i32, - width: u32, - height: u32, -) -> Result { - let area_x = (((x + display_info.x) as f32) * display_info.scale_factor) as i32; - let area_y = (((y + display_info.y) as f32) * display_info.scale_factor) as i32; - let area_width = (width as f32) * display_info.scale_factor; - let area_height = (height as f32) * display_info.scale_factor; - - wayland_screenshot(area_x, area_y, area_width as i32, area_height as i32) -} diff --git a/src/linux/wayland_screenshot.rs b/src/linux/wayland_capture.rs similarity index 78% rename from src/linux/wayland_screenshot.rs rename to src/linux/wayland_capture.rs index b579caa..7522fa3 100644 --- a/src/linux/wayland_screenshot.rs +++ b/src/linux/wayland_capture.rs @@ -1,12 +1,9 @@ -use crate::image_utils::png_to_rgba_image; -use anyhow::{anyhow, Result}; use dbus::{ arg::{AppendAll, Iter, IterAppend, PropMap, ReadAll, RefArg, TypeMismatchError, Variant}, blocking::Connection, message::{MatchRule, SignalArgs}, }; use image::RgbaImage; -use libwayshot::{CaptureRegion, WayshotConnection}; use percent_encoding::percent_decode; use std::{ collections::HashMap, @@ -16,10 +13,14 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; +use crate::error::{XCapError, XCapResult}; + +use super::{impl_monitor::ImplMonitor, utils::png_to_rgba_image}; + #[derive(Debug)] -pub struct OrgFreedesktopPortalRequestResponse { - pub status: u32, - pub results: PropMap, +struct OrgFreedesktopPortalRequestResponse { + status: u32, + results: PropMap, } impl AppendAll for OrgFreedesktopPortalRequestResponse { @@ -49,7 +50,7 @@ fn org_gnome_shell_screenshot( y: i32, width: i32, height: i32, -) -> Result { +) -> XCapResult { let proxy = conn.with_proxy( "org.gnome.Shell.Screenshot", "/org/gnome/Shell/Screenshot", @@ -86,7 +87,7 @@ fn org_freedesktop_portal_screenshot( y: i32, width: i32, height: i32, -) -> Result { +) -> XCapResult { let status: Arc>> = Arc::new(Mutex::new(None)); let status_res = status.clone(); let path: Arc> = Arc::new(Mutex::new(String::new())); @@ -134,7 +135,7 @@ fn org_freedesktop_portal_screenshot( let result = conn.process(Duration::from_millis(1000))?; let status = status_res .lock() - .map_err(|_| anyhow!("Get status lock failed"))?; + .map_err(|_| XCapError::new("Get status lock failed"))?; if result && status.is_some() { break; @@ -143,19 +144,19 @@ fn org_freedesktop_portal_screenshot( let status = status_res .lock() - .map_err(|_| anyhow!("Get status lock failed"))?; + .map_err(|_| XCapError::new("Get status lock failed"))?; let status = *status; let path = path_res .lock() - .map_err(|_| anyhow!("Get path lock failed"))?; + .map_err(|_| XCapError::new("Get path lock failed"))?; let path = &*path; if status.ne(&Some(0)) || path.is_empty() { if !path.is_empty() { fs::remove_file(path)?; } - return Err(anyhow!("Screenshot failed or canceled",)); + return Err(XCapError::new("Screenshot failed or canceled")); } let filename = percent_decode(path.as_bytes()).decode_utf8()?.to_string(); @@ -166,30 +167,14 @@ fn org_freedesktop_portal_screenshot( Ok(rgba_image) } -fn wlr_screenshot( - x_coordinate: i32, - y_coordinate: i32, - width: i32, - height: i32, -) -> Result { - let wayshot_connection = WayshotConnection::new()?; - let capture_region = CaptureRegion { - x_coordinate, - y_coordinate, - width, - height, - }; - let rgba_image = wayshot_connection.screenshot(capture_region, false)?; - - Ok(rgba_image) -} +pub fn wayland_capture(impl_monitor: &ImplMonitor) -> XCapResult { + let x = ((impl_monitor.x as f32) * impl_monitor.scale_factor) as i32; + let y = ((impl_monitor.y as f32) * impl_monitor.scale_factor) as i32; + let width = ((impl_monitor.width as f32) * impl_monitor.scale_factor) as i32; + let height = ((impl_monitor.height as f32) * impl_monitor.scale_factor) as i32; -// TODO: 失败后尝试删除文件 -pub fn wayland_screenshot(x: i32, y: i32, width: i32, height: i32) -> Result { let conn = Connection::new_session()?; - // TODO: work out if compositor is wlroots before attempting anything else org_gnome_shell_screenshot(&conn, x, y, width, height) .or_else(|_| org_freedesktop_portal_screenshot(&conn, x, y, width, height)) - .or_else(|_| wlr_screenshot(x, y, width, height)) } diff --git a/src/linux/xorg.rs b/src/linux/xorg_capture.rs similarity index 66% rename from src/linux/xorg.rs rename to src/linux/xorg_capture.rs index 3ad8548..ace6fb9 100644 --- a/src/linux/xorg.rs +++ b/src/linux/xorg_capture.rs @@ -1,8 +1,13 @@ -use crate::image_utils::vec_to_rgba_image; -use anyhow::{anyhow, Result}; -use display_info::DisplayInfo; use image::RgbaImage; -use xcb::x::{Drawable, GetImage, ImageFormat, ImageOrder}; +use xcb::{ + x::{Drawable, GetImage, ImageFormat, ImageOrder, Window}, + Connection, +}; + +use crate::{ + error::{XCapError, XCapResult}, + utils::image::vec_to_rgba_image, +}; fn get_pixel8_rgba( bytes: &[u8], @@ -67,18 +72,20 @@ fn get_pixel24_32_rgba( } } -fn capture(x: i32, y: i32, width: u32, height: u32) -> Result { - let (conn, index) = xcb::Connection::connect(None)?; +pub fn xorg_capture( + window: Window, + x: i32, + y: i32, + width: u32, + height: u32, +) -> XCapResult { + let (conn, _) = Connection::connect(None)?; let setup = conn.get_setup(); - let screen = setup - .roots() - .nth(index as usize) - .ok_or_else(|| anyhow!("Not found screen"))?; let get_image_cookie = conn.send_request(&GetImage { format: ImageFormat::ZPixmap, - drawable: Drawable::Window(screen.root()), + drawable: Drawable::Window(window), x: x as i16, y: y as i16, width: width as u16, @@ -95,7 +102,7 @@ fn capture(x: i32, y: i32, width: u32, height: u32) -> Result { .pixmap_formats() .iter() .find(|item| item.depth() == depth) - .ok_or(anyhow!("Not found pixmap format"))?; + .ok_or(XCapError::new("Not found pixmap format"))?; let bits_per_pixel = pixmap_format.bits_per_pixel() as u32; let bit_order = setup.bitmap_format_bit_order(); @@ -105,7 +112,7 @@ fn capture(x: i32, y: i32, width: u32, height: u32) -> Result { 16 => get_pixel16_rgba, 24 => get_pixel24_32_rgba, 32 => get_pixel24_32_rgba, - _ => return Err(anyhow!("Unsupported {} depth", depth)), + _ => return Err(XCapError::new(format!("Unsupported {} depth", depth))), }; for y in 0..height { @@ -122,27 +129,3 @@ fn capture(x: i32, y: i32, width: u32, height: u32) -> Result { vec_to_rgba_image(width, height, rgba) } - -pub fn xorg_capture_screen(display_info: &DisplayInfo) -> Result { - let x = ((display_info.x as f32) * display_info.scale_factor) as i32; - let y = ((display_info.y as f32) * display_info.scale_factor) as i32; - let width = ((display_info.width as f32) * display_info.scale_factor) as u32; - let height = ((display_info.height as f32) * display_info.scale_factor) as u32; - - capture(x, y, width, height) -} - -pub fn xorg_capture_screen_area( - display_info: &DisplayInfo, - x: i32, - y: i32, - width: u32, - height: u32, -) -> Result { - let area_x = (((x + display_info.x) as f32) * display_info.scale_factor) as i32; - let area_y = (((y + display_info.y) as f32) * display_info.scale_factor) as i32; - let area_width = ((width as f32) * display_info.scale_factor) as u32; - let area_height = ((height as f32) * display_info.scale_factor) as u32; - - capture(area_x, area_y, area_width, area_height) -} diff --git a/src/macos/capture.rs b/src/macos/capture.rs new file mode 100644 index 0000000..2ee3f6d --- /dev/null +++ b/src/macos/capture.rs @@ -0,0 +1,31 @@ +use core_graphics::{ + display::{kCGWindowImageDefault, CGWindowID, CGWindowListOption}, + geometry::CGRect, + window::create_image, +}; +use image::RgbaImage; + +use crate::{ + error::{XCapError, XCapResult}, + utils::image::{bgra_to_rgba_image, remove_extra_data}, +}; + +pub fn capture( + cg_rect: CGRect, + list_option: CGWindowListOption, + window_id: CGWindowID, +) -> XCapResult { + let cg_image = create_image(cg_rect, list_option, window_id, kCGWindowImageDefault) + .ok_or_else(|| XCapError::new(format!("Capture failed {} {:?}", window_id, cg_rect)))?; + + let width = cg_image.width(); + let height = cg_image.height(); + let clean_buf = remove_extra_data( + width, + height, + cg_image.bytes_per_row(), + Vec::from(cg_image.data().bytes()), + ); + + bgra_to_rgba_image(width as u32, height as u32, clean_buf) +} diff --git a/src/macos/impl_monitor.rs b/src/macos/impl_monitor.rs new file mode 100644 index 0000000..d45569c --- /dev/null +++ b/src/macos/impl_monitor.rs @@ -0,0 +1,128 @@ +use core_graphics::display::{ + kCGNullWindowID, kCGWindowListOptionAll, CGDirectDisplayID, CGDisplay, CGDisplayMode, CGError, + CGPoint, +}; +use image::RgbaImage; + +use crate::error::{XCapError, XCapResult}; + +use super::capture::capture; + +#[derive(Debug, Clone)] +pub(crate) struct ImplMonitor { + pub cg_display: CGDisplay, + pub id: u32, + pub name: String, + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, + pub rotation: f32, + pub scale_factor: f32, + pub frequency: f32, + pub is_primary: bool, +} + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + fn CGGetDisplaysWithPoint( + point: CGPoint, + max_displays: u32, + displays: *mut CGDirectDisplayID, + display_count: *mut u32, + ) -> CGError; +} + +impl ImplMonitor { + fn new(id: CGDirectDisplayID) -> XCapResult { + let cg_display = CGDisplay::new(id); + let screen_num = cg_display.model_number(); + let cg_rect = cg_display.bounds(); + let cg_display_mode = get_cg_display_mode(cg_display)?; + let pixel_width = cg_display_mode.pixel_width(); + let scale_factor = pixel_width as f32 / cg_rect.size.width as f32; + + Ok(ImplMonitor { + cg_display, + id: cg_display.id, + name: format!("Monitor #{screen_num}"), + x: cg_rect.origin.x as i32, + y: cg_rect.origin.y as i32, + width: cg_rect.size.width as u32, + height: cg_rect.size.height as u32, + rotation: cg_display.rotation() as f32, + scale_factor, + frequency: cg_display_mode.refresh_rate() as f32, + is_primary: cg_display.is_main(), + }) + } + pub fn all() -> XCapResult> { + // active vs online https://developer.apple.com/documentation/coregraphics/1454964-cggetonlinedisplaylist?language=objc + let display_ids = CGDisplay::active_displays()?; + + let mut impl_monitors: Vec = Vec::with_capacity(display_ids.len()); + + for display_id in display_ids { + impl_monitors.push(ImplMonitor::new(display_id)?); + } + + Ok(impl_monitors) + } + + pub fn from_point(x: i32, y: i32) -> XCapResult { + let point = CGPoint { + x: x as f64, + y: y as f64, + }; + let max_displays: u32 = 16; + let mut display_ids: Vec = vec![0; max_displays as usize]; + let mut display_count: u32 = 0; + + let cg_error = unsafe { + CGGetDisplaysWithPoint( + point, + max_displays, + display_ids.as_mut_ptr(), + &mut display_count, + ) + }; + + if cg_error != 0 { + return Err(XCapError::CoreGraphicsDisplayCGError(cg_error)); + } + + if display_count == 0 { + return Err(XCapError::new("Get displays from point failed")); + } + + let display_id = display_ids + .first() + .ok_or(XCapError::new("Monitor not found"))?; + + let impl_monitor = ImplMonitor::new(*display_id)?; + + if !impl_monitor.cg_display.is_active() { + Err(XCapError::new("Monitor is not active")) + } else { + Ok(impl_monitor) + } + } +} + +fn get_cg_display_mode(cg_display: CGDisplay) -> XCapResult { + let cg_display_mode = cg_display + .display_mode() + .ok_or_else(|| XCapError::new("Get display mode failed"))?; + + Ok(cg_display_mode) +} + +impl ImplMonitor { + pub fn capture_image(&self) -> XCapResult { + capture( + self.cg_display.bounds(), + kCGWindowListOptionAll, + kCGNullWindowID, + ) + } +} diff --git a/src/macos/impl_window.rs b/src/macos/impl_window.rs new file mode 100644 index 0000000..12ebd6f --- /dev/null +++ b/src/macos/impl_window.rs @@ -0,0 +1,182 @@ +use core_foundation::{ + array::{CFArrayGetCount, CFArrayGetValueAtIndex}, + base::{FromVoid, TCFType}, + dictionary::{CFDictionaryGetValue, CFDictionaryRef}, + number::{kCFNumberIntType, CFBooleanGetValue, CFBooleanRef, CFNumberGetValue, CFNumberRef}, + string::CFString, +}; +use core_graphics::{ + display::{ + kCGNullWindowID, kCGWindowListExcludeDesktopElements, kCGWindowListOptionIncludingWindow, + kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo, + }, + geometry::CGRect, + window::kCGWindowSharingNone, +}; +use image::RgbaImage; +use std::ffi::c_void; + +use crate::error::XCapResult; + +use super::{capture::capture, impl_monitor::ImplMonitor}; + +#[derive(Debug, Clone)] +pub(crate) struct ImplWindow { + pub window_cf_dictionary_ref: CFDictionaryRef, + pub id: u32, + pub title: String, + pub app_name: String, + pub current_monitor: ImplMonitor, + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, + pub is_minimized: bool, + pub is_maximized: bool, +} + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + fn CGRectMakeWithDictionaryRepresentation( + dict: CFDictionaryRef, + rect: &mut CGRect, + ) -> CFBooleanRef; +} + +fn get_cf_dictionary_get_value(cf_dictionary_ref: CFDictionaryRef, key: &str) -> *const c_void { + unsafe { + let cf_dictionary_key = CFString::new(key); + + CFDictionaryGetValue(cf_dictionary_ref, cf_dictionary_key.as_CFTypeRef()) + } +} + +fn get_window_cg_rect(window_cf_dictionary_ref: CFDictionaryRef) -> CGRect { + unsafe { + let window_bounds_ref = + get_cf_dictionary_get_value(window_cf_dictionary_ref, "kCGWindowBounds") + as CFDictionaryRef; + + let mut cg_rect = CGRect::default(); + CGRectMakeWithDictionaryRepresentation(window_bounds_ref, &mut cg_rect); + cg_rect + } +} + +impl ImplWindow { + pub fn new(window_cf_dictionary_ref: CFDictionaryRef) -> XCapResult { + unsafe { + let id = { + let cf_number_ref = + get_cf_dictionary_get_value(window_cf_dictionary_ref, "kCGWindowNumber") + as CFNumberRef; + + let mut window_id: u32 = 0; + CFNumberGetValue( + cf_number_ref, + kCFNumberIntType, + &mut window_id as *mut _ as *mut c_void, + ); + window_id + }; + + let title = { + let window_title_ref = + get_cf_dictionary_get_value(window_cf_dictionary_ref, "kCGWindowName"); + CFString::from_void(window_title_ref).to_string() + }; + + let app_name = { + let window_owner_name_ref = + get_cf_dictionary_get_value(window_cf_dictionary_ref, "kCGWindowOwnerName"); + CFString::from_void(window_owner_name_ref).to_string() + }; + + let cg_rect = get_window_cg_rect(window_cf_dictionary_ref); + + let is_minimized = { + let window_is_on_screen_ref = + get_cf_dictionary_get_value(window_cf_dictionary_ref, "kCGWindowIsOnscreen"); + !CFBooleanGetValue(window_is_on_screen_ref as CFBooleanRef) + }; + + let (is_maximized, current_monitor) = { + // 获取窗口中心点的坐标 + let window_center_x = (cg_rect.origin.x + cg_rect.size.width) / 2.0; + let window_center_y = (cg_rect.origin.y + cg_rect.size.height) / 2.0; + + let impl_monitor = + ImplMonitor::from_point(window_center_x as i32, window_center_y as i32)?; + + ( + cg_rect.size.width as u32 == impl_monitor.width + && cg_rect.size.height as u32 == impl_monitor.height, + impl_monitor, + ) + }; + + Ok(ImplWindow { + window_cf_dictionary_ref, + id, + title, + app_name, + current_monitor, + x: cg_rect.origin.x as i32, + y: cg_rect.origin.y as i32, + width: cg_rect.size.width as u32, + height: cg_rect.size.height as u32, + is_minimized, + is_maximized, + }) + } + } + + pub fn all() -> XCapResult> { + unsafe { + let cg_window_list_copy_window_info = CGWindowListCopyWindowInfo( + kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, + kCGNullWindowID, + ); + let num_windows = CFArrayGetCount(cg_window_list_copy_window_info); + + let mut impl_windows = Vec::new(); + + for i in 0..num_windows { + let window_cf_dictionary_ref = + CFArrayGetValueAtIndex(cg_window_list_copy_window_info, i) as CFDictionaryRef; + + if window_cf_dictionary_ref.is_null() { + continue; + } + + let window_sharing_state_ref = + get_cf_dictionary_get_value(window_cf_dictionary_ref, "kCGWindowSharingState"); + + let mut window_sharing_state: u32 = 0; + CFNumberGetValue( + window_sharing_state_ref as CFNumberRef, + kCFNumberIntType, + &mut window_sharing_state as *mut _ as *mut c_void, + ); + + if window_sharing_state == kCGWindowSharingNone { + continue; + } + + impl_windows.push(ImplWindow::new(window_cf_dictionary_ref)?); + } + + Ok(impl_windows) + } + } +} + +impl ImplWindow { + pub fn capture_image(&self) -> XCapResult { + capture( + get_window_cg_rect(self.window_cf_dictionary_ref), + kCGWindowListOptionIncludingWindow, + self.id, + ) + } +} diff --git a/src/macos/mod.rs b/src/macos/mod.rs new file mode 100644 index 0000000..42f354e --- /dev/null +++ b/src/macos/mod.rs @@ -0,0 +1,4 @@ +mod capture; + +pub mod impl_monitor; +pub mod impl_window; diff --git a/src/monitor.rs b/src/monitor.rs new file mode 100644 index 0000000..8d089f7 --- /dev/null +++ b/src/monitor.rs @@ -0,0 +1,81 @@ +use image::RgbaImage; + +use crate::{error::XCapResult, platform::impl_monitor::ImplMonitor}; + +#[derive(Debug, Clone)] +pub struct Monitor { + pub(crate) impl_monitor: ImplMonitor, +} + +impl Monitor { + pub(crate) fn new(impl_monitor: ImplMonitor) -> Monitor { + Monitor { impl_monitor } + } +} + +impl Monitor { + pub fn all() -> XCapResult> { + let monitors = ImplMonitor::all()? + .iter() + .map(|impl_monitor| Monitor::new(impl_monitor.clone())) + .collect(); + + Ok(monitors) + } + + pub fn from_point(x: i32, y: i32) -> XCapResult { + let impl_monitor = ImplMonitor::from_point(x, y)?; + + Ok(Monitor::new(impl_monitor)) + } +} + +impl Monitor { + /// Unique identifier associated with the screen. + pub fn id(&self) -> u32 { + self.impl_monitor.id + } + /// Unique identifier associated with the screen. + pub fn name(&self) -> &str { + &self.impl_monitor.name + } + /// The screen x coordinate. + pub fn x(&self) -> i32 { + self.impl_monitor.x + } + /// The screen x coordinate. + pub fn y(&self) -> i32 { + self.impl_monitor.y + } + /// The screen pixel width. + pub fn width(&self) -> u32 { + self.impl_monitor.width + } + /// The screen pixel height. + pub fn height(&self) -> u32 { + self.impl_monitor.height + } + /// Can be 0, 90, 180, 270, represents screen rotation in clock-wise degrees. + pub fn rotation(&self) -> f32 { + self.impl_monitor.rotation + } + /// Output device's pixel scale factor. + pub fn scale_factor(&self) -> f32 { + self.impl_monitor.scale_factor + } + /// The screen refresh rate. + pub fn frequency(&self) -> f32 { + self.impl_monitor.frequency + } + /// Whether the screen is the main screen + pub fn is_primary(&self) -> bool { + self.impl_monitor.is_primary + } +} + +impl Monitor { + /// Capture image of the monitor + pub fn capture_image(&self) -> XCapResult { + self.impl_monitor.capture_image() + } +} diff --git a/src/image_utils.rs b/src/utils/image.rs similarity index 72% rename from src/image_utils.rs rename to src/utils/image.rs index d71a7f8..1600792 100644 --- a/src/image_utils.rs +++ b/src/utils/image.rs @@ -1,12 +1,13 @@ -use anyhow::{anyhow, Result}; use image::RgbaImage; -pub fn vec_to_rgba_image(width: u32, height: u32, buf: Vec) -> Result { - RgbaImage::from_vec(width, height, buf).ok_or(anyhow!("buffer not big enough")) +use crate::error::{XCapError, XCapResult}; + +pub fn vec_to_rgba_image(width: u32, height: u32, buf: Vec) -> XCapResult { + RgbaImage::from_vec(width, height, buf).ok_or_else(|| XCapError::new("buffer not big enough")) } #[cfg(any(target_os = "windows", target_os = "macos", test))] -pub fn bgra_to_rgba_image(width: u32, height: u32, buf: Vec) -> Result { +pub fn bgra_to_rgba_image(width: u32, height: u32, buf: Vec) -> XCapResult { let mut rgba_buf = buf.clone(); for (src, dst) in buf.chunks_exact(4).zip(rgba_buf.chunks_exact_mut(4)) { @@ -21,8 +22,8 @@ pub fn bgra_to_rgba_image(width: u32, height: u32, buf: Vec) -> Result Result { - use image::open; - - let mut dynamic_image = open(filename)?; - dynamic_image = dynamic_image.crop(x as u32, y as u32, width as u32, height as u32); - Ok(dynamic_image.to_rgba8()) -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..14995d4 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod image; diff --git a/src/win32.rs b/src/win32.rs deleted file mode 100644 index 9dd7e2d..0000000 --- a/src/win32.rs +++ /dev/null @@ -1,247 +0,0 @@ -use crate::image_utils::bgra_to_rgba_image; -use anyhow::{anyhow, Result}; -use display_info::DisplayInfo; -use fxhash::hash32; -use image::RgbaImage; -use std::{mem, ops::Deref, ptr}; -use widestring::U16CString; -use windows::{ - core::PCWSTR, - Win32::{ - Foundation::{BOOL, LPARAM, RECT}, - Graphics::Gdi::{ - CreateCompatibleBitmap, CreateCompatibleDC, CreateDCW, DeleteDC, DeleteObject, - EnumDisplayMonitors, GetDIBits, GetMonitorInfoW, GetObjectW, SelectObject, - SetStretchBltMode, StretchBlt, BITMAP, BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, - HBITMAP, HDC, HMONITOR, MONITORINFOEXW, RGBQUAD, SRCCOPY, STRETCH_HALFTONE, - }, - }, -}; - -// 自动释放资源 -macro_rules! drop_box { - ($type:tt, $value:expr, $drop:expr) => {{ - struct DropBox($type); - - impl Deref for DropBox { - type Target = $type; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl Drop for DropBox { - fn drop(&mut self) { - $drop(self.0); - } - } - - DropBox($value) - }}; -} - -fn get_monitor_info_exw(h_monitor: HMONITOR) -> Result { - let mut monitor_info_exw: MONITORINFOEXW = unsafe { mem::zeroed() }; - monitor_info_exw.monitorInfo.cbSize = mem::size_of::() as u32; - let monitor_info_exw_ptr = <*mut _>::cast(&mut monitor_info_exw); - - unsafe { - GetMonitorInfoW(h_monitor, monitor_info_exw_ptr).ok()?; - }; - Ok(monitor_info_exw) -} - -fn get_monitor_info_exw_from_id(id: u32) -> Result { - let monitor_info_exws: *mut Vec = Box::into_raw(Box::default()); - - unsafe { - EnumDisplayMonitors( - HDC::default(), - None, - Some(monitor_enum_proc), - LPARAM(monitor_info_exws as isize), - ) - .ok()?; - }; - - let monitor_info_exws_borrow = unsafe { &Box::from_raw(monitor_info_exws) }; - - let monitor_info_exw = monitor_info_exws_borrow - .iter() - .find(|&&monitor_info_exw| { - let sz_device_ptr = monitor_info_exw.szDevice.as_ptr(); - let sz_device_string = - unsafe { U16CString::from_ptr_str(sz_device_ptr).to_string_lossy() }; - hash32(sz_device_string.as_bytes()) == id - }) - .ok_or_else(|| anyhow!("Can't find a display by id {id}"))?; - - Ok(*monitor_info_exw) -} - -extern "system" fn monitor_enum_proc( - h_monitor: HMONITOR, - _: HDC, - _: *mut RECT, - state: LPARAM, -) -> BOOL { - let box_monitor_info_exw = unsafe { Box::from_raw(state.0 as *mut Vec) }; - let state = Box::leak(box_monitor_info_exw); - - match get_monitor_info_exw(h_monitor) { - Ok(monitor_info_exw) => { - state.push(monitor_info_exw); - BOOL::from(true) - } - Err(_) => BOOL::from(false), - } -} - -fn capture(display_id: u32, x: i32, y: i32, width: i32, height: i32) -> Result { - let monitor_info_exw = get_monitor_info_exw_from_id(display_id)?; - - let sz_device = monitor_info_exw.szDevice; - let sz_device_ptr = sz_device.as_ptr(); - - let dcw_drop_box = drop_box!( - HDC, - unsafe { - CreateDCW( - PCWSTR(sz_device_ptr), - PCWSTR(sz_device_ptr), - PCWSTR(ptr::null()), - None, - ) - }, - |dcw| unsafe { DeleteDC(dcw) } - ); - - let compatible_dc_drop_box = drop_box!( - HDC, - unsafe { CreateCompatibleDC(*dcw_drop_box) }, - |compatible_dc| unsafe { DeleteDC(compatible_dc) } - ); - - let h_bitmap_drop_box = drop_box!( - HBITMAP, - unsafe { CreateCompatibleBitmap(*dcw_drop_box, width, height) }, - |h_bitmap| unsafe { DeleteObject(h_bitmap) } - ); - - unsafe { - SelectObject(*compatible_dc_drop_box, *h_bitmap_drop_box); - SetStretchBltMode(*dcw_drop_box, STRETCH_HALFTONE); - }; - - unsafe { - StretchBlt( - *compatible_dc_drop_box, - 0, - 0, - width, - height, - *dcw_drop_box, - x, - y, - width, - height, - SRCCOPY, - ) - .ok()?; - }; - - let mut bitmap_info = BITMAPINFO { - bmiHeader: BITMAPINFOHEADER { - biSize: mem::size_of::() as u32, - biWidth: width, - biHeight: height, // 这里可以传递负数, 但是不知道为什么会报错 - biPlanes: 1, - biBitCount: 32, - biCompression: 0, - biSizeImage: 0, - biXPelsPerMeter: 0, - biYPelsPerMeter: 0, - biClrUsed: 0, - biClrImportant: 0, - }, - bmiColors: [RGBQUAD::default(); 1], - }; - - let data = vec![0u8; (width * height) as usize * 4]; - let buf_prt = data.as_ptr() as *mut _; - - let is_success = unsafe { - GetDIBits( - *compatible_dc_drop_box, - *h_bitmap_drop_box, - 0, - height as u32, - Some(buf_prt), - &mut bitmap_info, - DIB_RGB_COLORS, - ) == 0 - }; - - if is_success { - return Err(anyhow!("Get RGBA data failed")); - } - - let mut bitmap = BITMAP::default(); - let bitmap_ptr = <*mut _>::cast(&mut bitmap); - - unsafe { - // Get the BITMAP from the HBITMAP. - GetObjectW( - *h_bitmap_drop_box, - mem::size_of::() as i32, - Some(bitmap_ptr), - ); - } - - // 旋转图像,图像数据是倒置的 - let mut chunks: Vec> = data - .chunks(width as usize * 4) - .map(|x| x.to_vec()) - .collect(); - - chunks.reverse(); - - bgra_to_rgba_image( - bitmap.bmWidth as u32, - bitmap.bmHeight as u32, - chunks.concat(), - ) -} - -pub fn capture_screen(display_info: &DisplayInfo) -> Result { - let width = ((display_info.width as f32) * display_info.scale_factor) as i32; - let height = ((display_info.height as f32) * display_info.scale_factor) as i32; - - capture(display_info.id, 0, 0, width, height) -} - -pub fn capture_screen_area( - display_info: &DisplayInfo, - x: i32, - y: i32, - width: u32, - height: u32, -) -> Result { - let area_x = ((x as f32) * display_info.scale_factor) as i32; - let area_y = ((y as f32) * display_info.scale_factor) as i32; - let area_width = ((width as f32) * display_info.scale_factor) as i32; - let area_height = ((height as f32) * display_info.scale_factor) as i32; - - capture(display_info.id, area_x, area_y, area_width, area_height) -} - -pub fn capture_screen_area_ignore_sf( - display_info: &DisplayInfo, - x: i32, - y: i32, - width: u32, - height: u32, -) -> Result { - capture(display_info.id, x, y, width as i32, height as i32) -} diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 0000000..1bea183 --- /dev/null +++ b/src/window.rs @@ -0,0 +1,74 @@ +use image::RgbaImage; + +use crate::{error::XCapResult, platform::impl_window::ImplWindow, Monitor}; + +#[derive(Debug, Clone)] +pub struct Window { + pub(crate) impl_window: ImplWindow, +} + +impl Window { + pub(crate) fn new(impl_window: ImplWindow) -> Window { + Window { impl_window } + } +} + +impl Window { + pub fn all() -> XCapResult> { + let windows = ImplWindow::all()? + .iter() + .map(|impl_window| Window::new(impl_window.clone())) + .collect(); + + Ok(windows) + } +} + +impl Window { + // The window id + pub fn id(&self) -> u32 { + self.impl_window.id + } + /// The window app name + pub fn app_name(&self) -> &str { + &self.impl_window.app_name + } + /// The window title + pub fn title(&self) -> &str { + &self.impl_window.title + } + /// The window current monitor + pub fn current_monitor(&self) -> Monitor { + Monitor::new(self.impl_window.current_monitor.to_owned()) + } + /// The window x coordinate. + pub fn x(&self) -> i32 { + self.impl_window.x + } + /// The window x coordinate. + pub fn y(&self) -> i32 { + self.impl_window.y + } + /// The window pixel width. + pub fn width(&self) -> u32 { + self.impl_window.width + } + /// The window pixel height. + pub fn height(&self) -> u32 { + self.impl_window.height + } + /// The window is minimized. + pub fn is_minimized(&self) -> bool { + self.impl_window.is_minimized + } + /// The window is maximized. + pub fn is_maximized(&self) -> bool { + self.impl_window.is_maximized + } +} + +impl Window { + pub fn capture_image(&self) -> XCapResult { + self.impl_window.capture_image() + } +} diff --git a/src/windows/boxed.rs b/src/windows/boxed.rs new file mode 100644 index 0000000..aa42078 --- /dev/null +++ b/src/windows/boxed.rs @@ -0,0 +1,84 @@ +use std::{ops::Deref, ptr}; +use windows::{ + core::PCWSTR, + Win32::Graphics::Gdi::{CreateDCW, DeleteDC, DeleteObject, GetWindowDC, HBITMAP, HDC}, +}; + +use super::{impl_monitor::ImplMonitor, impl_window::ImplWindow}; + +pub(crate) struct BoxHDC(HDC); + +impl Deref for BoxHDC { + type Target = HDC; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Drop for BoxHDC { + fn drop(&mut self) { + unsafe { + DeleteDC(self.0); + }; + } +} + +impl BoxHDC { + pub fn new(hdc: HDC) -> Self { + BoxHDC(hdc) + } +} + +impl From<&[u16; 32]> for BoxHDC { + fn from(sz_device: &[u16; 32]) -> Self { + let sz_device_ptr = sz_device.as_ptr(); + + let hdc = unsafe { + CreateDCW( + PCWSTR(sz_device_ptr), + PCWSTR(sz_device_ptr), + PCWSTR(ptr::null()), + None, + ) + }; + + BoxHDC::new(hdc) + } +} + +impl From<&ImplMonitor> for BoxHDC { + fn from(impl_monitor: &ImplMonitor) -> Self { + BoxHDC::from(&impl_monitor.monitor_info_ex_w.szDevice) + } +} + +impl From<&ImplWindow> for BoxHDC { + fn from(impl_window: &ImplWindow) -> Self { + let hdc = unsafe { GetWindowDC(impl_window.hwnd) }; + + BoxHDC::new(hdc) + } +} + +pub(crate) struct BoxHBITMAP(HBITMAP); + +impl Deref for BoxHBITMAP { + type Target = HBITMAP; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Drop for BoxHBITMAP { + fn drop(&mut self) { + unsafe { + DeleteObject(self.0); + }; + } +} + +impl BoxHBITMAP { + pub fn new(h_bitmap: HBITMAP) -> Self { + BoxHBITMAP(h_bitmap) + } +} diff --git a/src/windows/capture.rs b/src/windows/capture.rs new file mode 100644 index 0000000..c844843 --- /dev/null +++ b/src/windows/capture.rs @@ -0,0 +1,126 @@ +use image::RgbaImage; +use std::mem; +use windows::Win32::{ + Graphics::Gdi::{ + BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, GetDIBits, SelectObject, BITMAPINFO, + BITMAPINFOHEADER, DIB_RGB_COLORS, RGBQUAD, SRCCOPY, + }, + Storage::Xps::{PrintWindow, PRINT_WINDOW_FLAGS, PW_CLIENTONLY}, + UI::WindowsAndMessaging::PW_RENDERFULLCONTENT, +}; + +use crate::{ + error::{XCapError, XCapResult}, + utils::image::bgra_to_rgba_image, +}; + +use super::{ + boxed::{BoxHBITMAP, BoxHDC}, + impl_monitor::ImplMonitor, + impl_window::ImplWindow, +}; + +fn to_rgba_image( + box_hdc_mem: BoxHDC, + box_h_bitmap: BoxHBITMAP, + width: i32, + height: i32, +) -> XCapResult { + let mut bitmap_info = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: mem::size_of::() as u32, + biWidth: width, + biHeight: -height, + biPlanes: 1, + biBitCount: 32, + biCompression: 0, + biSizeImage: 0, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }, + bmiColors: [RGBQUAD::default(); 1], + }; + + let data = vec![0u8; (width * height) as usize * 4]; + let buf_prt = data.as_ptr() as *mut _; + + unsafe { + // 读取数据到 buffer 中 + let is_success = GetDIBits( + *box_hdc_mem, + *box_h_bitmap, + 0, + height as u32, + Some(buf_prt), + &mut bitmap_info, + DIB_RGB_COLORS, + ) == 0; + + if is_success { + return Err(XCapError::new("Get RGBA data failed")); + } + }; + + bgra_to_rgba_image(width as u32, height as u32, data) +} + +#[allow(unused)] +pub fn capture_monitor( + impl_monitor: &ImplMonitor, + x: i32, + y: i32, + width: i32, + height: i32, +) -> XCapResult { + unsafe { + let box_hdc_monitor = BoxHDC::from(impl_monitor); + // 内存中的HDC + let box_hdc_mem = BoxHDC::new(CreateCompatibleDC(*box_hdc_monitor)); + let box_h_bitmap = BoxHBITMAP::new(CreateCompatibleBitmap(*box_hdc_monitor, width, height)); + + // 使用SelectObject函数将这个位图选择到DC中 + SelectObject(*box_hdc_mem, *box_h_bitmap); + + // 拷贝原始图像到内存 + // 咋合理不需要i缩放图片,所以直接使用BitBlt + // 如需要缩放,则使用 StretchBlt + BitBlt( + *box_hdc_mem, + 0, + 0, + width, + height, + *box_hdc_monitor, + x, + y, + SRCCOPY, + )?; + + to_rgba_image(box_hdc_mem, box_h_bitmap, width, height) + } +} + +#[allow(unused)] +pub fn capture_window(impl_window: &ImplWindow, width: i32, height: i32) -> XCapResult { + unsafe { + let box_hdc_window: BoxHDC = BoxHDC::from(impl_window); + // 内存中的HDC + let box_hdc_mem = BoxHDC::new(CreateCompatibleDC(*box_hdc_window)); + let box_h_bitmap = BoxHBITMAP::new(CreateCompatibleBitmap(*box_hdc_window, width, height)); + + // 使用SelectObject函数将这个位图选择到DC中 + SelectObject(*box_hdc_mem, *box_h_bitmap); + + // Grab a copy of the window. Use PrintWindow because it works even when the + // window's partially occluded. The PW_RENDERFULLCONTENT flag is undocumented, + // but works starting in Windows 8.1. It allows for capturing the contents of + // the window that are drawn using DirectComposition. + // https://github.com/chromium/chromium/blob/main/ui/snapshot/snapshot_win.cc#L39-L45 + let flags = PW_CLIENTONLY.0 | PW_RENDERFULLCONTENT; + PrintWindow(impl_window.hwnd, *box_hdc_mem, PRINT_WINDOW_FLAGS(flags)); + + to_rgba_image(box_hdc_mem, box_h_bitmap, width, height) + } +} diff --git a/src/windows/impl_monitor.rs b/src/windows/impl_monitor.rs new file mode 100644 index 0000000..8110dcb --- /dev/null +++ b/src/windows/impl_monitor.rs @@ -0,0 +1,166 @@ +use image::RgbaImage; +use std::mem; +use windows::{ + core::PCWSTR, + Win32::{ + Foundation::{BOOL, LPARAM, POINT, RECT, TRUE}, + Graphics::Gdi::{ + EnumDisplayMonitors, EnumDisplaySettingsExW, GetDeviceCaps, GetMonitorInfoW, + MonitorFromPoint, DESKTOPHORZRES, DEVMODEW, DEVMODE_DISPLAY_ORIENTATION, EDS_RAWMODE, + ENUM_CURRENT_SETTINGS, HDC, HMONITOR, HORZRES, MONITORINFO, MONITORINFOEXW, + MONITOR_DEFAULTTONULL, + }, + UI::WindowsAndMessaging::MONITORINFOF_PRIMARY, + }, +}; + +use crate::error::{XCapError, XCapResult}; + +use super::{boxed::BoxHDC, capture::capture_monitor, utils::wide_string_to_string}; + +// A函数与W函数区别 +// https://learn.microsoft.com/zh-cn/windows/win32/learnwin32/working-with-strings + +#[derive(Debug, Clone)] +pub(crate) struct ImplMonitor { + #[allow(unused)] + pub hmonitor: HMONITOR, + pub monitor_info_ex_w: MONITORINFOEXW, + pub id: u32, + pub name: String, + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, + pub rotation: f32, + pub scale_factor: f32, + pub frequency: f32, + pub is_primary: bool, +} + +extern "system" fn monitor_enum_proc( + hmonitor: HMONITOR, + _: HDC, + _: *mut RECT, + state: LPARAM, +) -> BOOL { + unsafe { + let state = Box::leak(Box::from_raw(state.0 as *mut Vec)); + state.push(hmonitor); + + TRUE + } +} + +fn get_dev_mode_w(monitor_info_exw: &MONITORINFOEXW) -> XCapResult { + let sz_device = monitor_info_exw.szDevice.as_ptr(); + let mut dev_mode_w = DEVMODEW::default(); + dev_mode_w.dmSize = mem::size_of::() as u16; + + unsafe { + EnumDisplaySettingsExW( + PCWSTR(sz_device), + ENUM_CURRENT_SETTINGS, + &mut dev_mode_w, + EDS_RAWMODE, + ) + .ok()?; + }; + + Ok(dev_mode_w) +} + +impl ImplMonitor { + pub fn new(hmonitor: HMONITOR) -> XCapResult { + let mut monitor_info_ex_w = MONITORINFOEXW::default(); + monitor_info_ex_w.monitorInfo.cbSize = mem::size_of::() as u32; + let monitor_info_ex_w_ptr = + &mut monitor_info_ex_w as *mut MONITORINFOEXW as *mut MONITORINFO; + + // https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-getmonitorinfoa + unsafe { GetMonitorInfoW(hmonitor, monitor_info_ex_w_ptr).ok()? }; + let rc_monitor = monitor_info_ex_w.monitorInfo.rcMonitor; + + let dev_mode_w = get_dev_mode_w(&monitor_info_ex_w)?; + + let dm_display_orientation = + unsafe { dev_mode_w.Anonymous1.Anonymous2.dmDisplayOrientation }; + + let rotation = match dm_display_orientation { + DEVMODE_DISPLAY_ORIENTATION(0) => 0.0, + DEVMODE_DISPLAY_ORIENTATION(1) => 90.0, + DEVMODE_DISPLAY_ORIENTATION(2) => 180.0, + DEVMODE_DISPLAY_ORIENTATION(3) => 270.0, + _ => dm_display_orientation.0 as f32, + }; + + let dev_mode_w = get_dev_mode_w(&monitor_info_ex_w)?; + + let box_hdc_monitor = BoxHDC::from(&monitor_info_ex_w.szDevice); + + let scale_factor = unsafe { + let physical_width = GetDeviceCaps(*box_hdc_monitor, DESKTOPHORZRES); + let logical_width = GetDeviceCaps(*box_hdc_monitor, HORZRES); + + physical_width as f32 / logical_width as f32 + }; + + Ok(ImplMonitor { + hmonitor, + monitor_info_ex_w, + id: hmonitor.0 as u32, + name: wide_string_to_string(&monitor_info_ex_w.szDevice)?, + x: rc_monitor.left, + y: rc_monitor.top, + width: (rc_monitor.right - rc_monitor.left) as u32, + height: (rc_monitor.bottom - rc_monitor.top) as u32, + rotation, + scale_factor, + frequency: dev_mode_w.dmDisplayFrequency as f32, + is_primary: monitor_info_ex_w.monitorInfo.dwFlags == MONITORINFOF_PRIMARY, + }) + } + + pub fn all() -> XCapResult> { + let hmonitors_mut_ptr: *mut Vec = Box::into_raw(Box::default()); + + let hmonitors = unsafe { + EnumDisplayMonitors( + HDC::default(), + None, + Some(monitor_enum_proc), + LPARAM(hmonitors_mut_ptr as isize), + ) + .ok()?; + Box::from_raw(hmonitors_mut_ptr) + }; + + let mut impl_monitors = Vec::with_capacity(hmonitors.len()); + + for &hmonitor in hmonitors.iter() { + impl_monitors.push(ImplMonitor::new(hmonitor)?); + } + + Ok(impl_monitors) + } + + pub fn from_point(x: i32, y: i32) -> XCapResult { + let point = POINT { x, y }; + let hmonitor = unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONULL) }; + + if hmonitor.is_invalid() { + return Err(XCapError::new("Not found monitor")); + } + + ImplMonitor::new(hmonitor) + } +} + +impl ImplMonitor { + pub fn capture_image(&self) -> XCapResult { + let width = ((self.width as f32) * self.scale_factor) as i32; + let height = ((self.height as f32) * self.scale_factor) as i32; + + capture_monitor(self, 0, 0, width, height) + } +} diff --git a/src/windows/impl_window.rs b/src/windows/impl_window.rs new file mode 100644 index 0000000..f77177f --- /dev/null +++ b/src/windows/impl_window.rs @@ -0,0 +1,168 @@ +use image::RgbaImage; +use std::{ffi::c_void, mem}; +use windows::Win32::{ + Foundation::{BOOL, HWND, LPARAM, TRUE}, + Graphics::{ + Dwm::{DwmGetWindowAttribute, DWMWA_CLOAKED, DWM_CLOAKED_SHELL}, + Gdi::{MonitorFromWindow, MONITOR_DEFAULTTONEAREST}, + }, + UI::WindowsAndMessaging::{ + EnumWindows, GetAncestor, GetLastActivePopup, GetWindowInfo, GetWindowLongW, + GetWindowTextLengthW, GetWindowTextW, IsIconic, IsWindowVisible, IsZoomed, GA_ROOTOWNER, + GWL_EXSTYLE, WINDOWINFO, WINDOW_EX_STYLE, WS_EX_TOOLWINDOW, + }, +}; + +use crate::error::XCapResult; + +use super::{capture::capture_window, impl_monitor::ImplMonitor, utils::wide_string_to_string}; + +#[derive(Debug, Clone)] +pub(crate) struct ImplWindow { + pub hwnd: HWND, + #[allow(unused)] + pub window_info: WINDOWINFO, + pub id: u32, + pub title: String, + pub app_name: String, + pub current_monitor: ImplMonitor, + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, + pub is_minimized: bool, + pub is_maximized: bool, +} + +fn is_valid_window(hwnd: HWND) -> bool { + unsafe { + // ignore invisible windows + if !IsWindowVisible(hwnd).as_bool() { + return false; + } + + // ignore windows in other virtual desktops + let mut cloaked = 0u32; + + let is_dwm_get_window_attribute_fail = DwmGetWindowAttribute( + hwnd, + DWMWA_CLOAKED, + &mut cloaked as *mut u32 as *mut c_void, + mem::size_of::() as u32, + ) + .is_err(); + + if is_dwm_get_window_attribute_fail { + return false; + } + + // windows in other virtual desktops have the DWM_CLOAKED_SHELL bit set + if cloaked & DWM_CLOAKED_SHELL != 0 { + return false; + } + + // https://stackoverflow.com/questions/7277366 + let mut hwnd_walk = None; + + // Start at the root owner + let mut hwnd_tray = GetAncestor(hwnd, GA_ROOTOWNER); + + // See if we are the last active visible popup + while Some(hwnd_tray) != hwnd_walk { + hwnd_walk = Some(hwnd_tray); + hwnd_tray = GetLastActivePopup(hwnd_tray); + + if IsWindowVisible(hwnd_tray).as_bool() { + break; + } + } + + if hwnd_walk != Some(hwnd) { + return false; + } + + // Tool windows should not be displayed either, these do not appear in the task bar. + let window_ex_style = GetWindowLongW(hwnd, GWL_EXSTYLE) as u32; + + if WINDOW_EX_STYLE(window_ex_style).contains(WS_EX_TOOLWINDOW) { + return false; + } + } + + true +} + +unsafe extern "system" fn enum_windows_proc(hwnd: HWND, state: LPARAM) -> BOOL { + if !is_valid_window(hwnd) { + return TRUE; + } + + let state = Box::leak(Box::from_raw(state.0 as *mut Vec)); + state.push(hwnd); + + TRUE +} + +impl ImplWindow { + fn new(hwnd: HWND) -> XCapResult { + unsafe { + let mut window_info = WINDOWINFO::default(); + window_info.cbSize = mem::size_of::() as u32; + + GetWindowInfo(hwnd, &mut window_info)?; + + let title = { + let text_length = GetWindowTextLengthW(hwnd); + let mut wide_buffer = vec![0u16; (text_length + 1) as usize]; + GetWindowTextW(hwnd, &mut wide_buffer); + wide_string_to_string(&wide_buffer)? + }; + + let hmonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + let rc_client = window_info.rcClient; + let is_minimized = IsIconic(hwnd).as_bool(); + let is_maximized = IsZoomed(hwnd).as_bool(); + + Ok(ImplWindow { + hwnd, + window_info, + id: hwnd.0 as u32, + title, + app_name: String::from("Unsupported"), + current_monitor: ImplMonitor::new(hmonitor)?, + x: rc_client.left, + y: rc_client.top, + width: (rc_client.right - rc_client.left) as u32, + height: (rc_client.bottom - rc_client.top) as u32, + is_minimized, + is_maximized, + }) + } + } + + pub fn all() -> XCapResult> { + let hwnds_mut_ptr: *mut Vec = Box::into_raw(Box::default()); + + let hwnds = unsafe { + EnumWindows(Some(enum_windows_proc), LPARAM(hwnds_mut_ptr as isize))?; + Box::from_raw(hwnds_mut_ptr) + }; + + let mut impl_windows = Vec::new(); + + for &hwnd in hwnds.iter() { + impl_windows.push(ImplWindow::new(hwnd)?); + } + + Ok(impl_windows) + } +} + +impl ImplWindow { + pub fn capture_image(&self) -> XCapResult { + let width = ((self.width as f32) * self.current_monitor.scale_factor) as i32; + let height = ((self.height as f32) * self.current_monitor.scale_factor) as i32; + + capture_window(self, width, height) + } +} diff --git a/src/windows/mod.rs b/src/windows/mod.rs new file mode 100644 index 0000000..115329e --- /dev/null +++ b/src/windows/mod.rs @@ -0,0 +1,6 @@ +mod boxed; +mod capture; +mod utils; + +pub mod impl_monitor; +pub mod impl_window; diff --git a/src/windows/utils.rs b/src/windows/utils.rs new file mode 100644 index 0000000..9649e69 --- /dev/null +++ b/src/windows/utils.rs @@ -0,0 +1,10 @@ +use crate::error::{XCapError, XCapResult}; + +pub(super) fn wide_string_to_string(wide_string: &[u16]) -> XCapResult { + if let Some(null_pos) = wide_string.iter().position(|pos| *pos == 0) { + let string = String::from_utf16(&wide_string[..null_pos])?; + return Ok(string); + } + + Err(XCapError::new("Convert wide string to string failed")) +}