diff --git a/Cargo.toml b/Cargo.toml index 3dd8cb3..85bd006 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["screen", "monitor", "window", "capture", "image"] [dependencies] image = "0.24" +log = "0.4" thiserror = "1.0" [target.'cfg(target_os = "macos")'.dependencies] @@ -26,6 +27,9 @@ windows = { version = "0.52", features = [ "Win32_Graphics_Dwm", "Win32_UI_WindowsAndMessaging", "Win32_Storage_Xps", + "Win32_System_Threading", + "Win32_System_ProcessStatus", + "Win32_Storage_FileSystem", ] } [target.'cfg(target_os="linux")'.dependencies] diff --git a/examples/monitor.rs b/examples/monitor.rs index 2939691..177acbe 100644 --- a/examples/monitor.rs +++ b/examples/monitor.rs @@ -8,10 +8,11 @@ fn main() { for monitor in monitors { println!( - "Monitor: {} {} {:?} {:?}", + "Monitor:\n id: {}\n name: {}\n position: {:?}\n size: {:?}\n state:{:?}\n", monitor.id(), monitor.name(), - (monitor.x(), monitor.y(), monitor.width(), monitor.height()), + (monitor.x(), monitor.y()), + (monitor.width(), monitor.height()), ( monitor.rotation(), monitor.scale_factor(), diff --git a/examples/window.rs b/examples/window.rs index 33ef299..2cccebb 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -8,12 +8,13 @@ fn main() { for window in windows { println!( - "Window: {} {} {} {:?} {:?} {:?}", + "Window:\n id: {}\n title: {}\n app_name: {}\n monitor: {:?}\n position: {:?}\n size {:?}\n state {:?}\n", window.id(), window.title(), window.app_name(), window.current_monitor().name(), - (window.x(), window.y(), window.width(), window.height()), + (window.x(), window.y()), + (window.width(), window.height()), (window.is_minimized(), window.is_maximized()) ); } diff --git a/src/lib.rs b/src/lib.rs index 8feda74..84db7f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ mod error; mod monitor; -mod utils; mod window; #[cfg(target_os = "macos")] diff --git a/src/linux/xorg_capture.rs b/src/linux/xorg_capture.rs index ace6fb9..55fbffb 100644 --- a/src/linux/xorg_capture.rs +++ b/src/linux/xorg_capture.rs @@ -4,10 +4,7 @@ use xcb::{ Connection, }; -use crate::{ - error::{XCapError, XCapResult}, - utils::image::vec_to_rgba_image, -}; +use crate::error::{XCapError, XCapResult}; fn get_pixel8_rgba( bytes: &[u8], @@ -97,7 +94,6 @@ pub fn xorg_capture( let bytes = get_image_reply.data(); let depth = get_image_reply.depth(); - let mut rgba = vec![0u8; (width * height * 4) as usize]; let pixmap_format = setup .pixmap_formats() .iter() @@ -115,6 +111,7 @@ pub fn xorg_capture( _ => return Err(XCapError::new(format!("Unsupported {} depth", depth))), }; + let mut rgba = vec![0u8; (width * height * 4) as usize]; for y in 0..height { for x in 0..width { let index = ((y * width + x) * 4) as usize; @@ -127,5 +124,6 @@ pub fn xorg_capture( } } - vec_to_rgba_image(width, height, rgba) + RgbaImage::from_raw(width, height, rgba) + .ok_or_else(|| XCapError::new("RgbaImage::from_raw failed")) } diff --git a/src/macos/capture.rs b/src/macos/capture.rs index 2ee3f6d..abe5bbc 100644 --- a/src/macos/capture.rs +++ b/src/macos/capture.rs @@ -5,10 +5,7 @@ use core_graphics::{ }; use image::RgbaImage; -use crate::{ - error::{XCapError, XCapResult}, - utils::image::{bgra_to_rgba_image, remove_extra_data}, -}; +use crate::error::{XCapError, XCapResult}; pub fn capture( cg_rect: CGRect, @@ -20,12 +17,21 @@ pub fn capture( 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()), - ); + let bytes = Vec::from(cg_image.data().bytes()); + + // Some platforms e.g. MacOS can have extra bytes at the end of each row. + // See + // https://github.com/nashaofu/xcap/issues/29 + // https://github.com/nashaofu/xcap/issues/38 + let mut buffer = Vec::with_capacity(width * height * 4); + for row in bytes.chunks_exact(cg_image.bytes_per_row()) { + buffer.extend_from_slice(&row[..width * 4]); + } + + for bgra in buffer.chunks_exact_mut(4) { + bgra.swap(0, 2); + } - bgra_to_rgba_image(width as u32, height as u32, clean_buf) + RgbaImage::from_raw(width as u32, height as u32, buffer) + .ok_or_else(|| XCapError::new("RgbaImage::from_raw failed")) } diff --git a/src/utils/image.rs b/src/utils/image.rs deleted file mode 100644 index 10193d1..0000000 --- a/src/utils/image.rs +++ /dev/null @@ -1,71 +0,0 @@ -use image::RgbaImage; - -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) -> XCapResult { - let mut rgba_buf = buf.clone(); - - for (src, dst) in buf.chunks_exact(4).zip(rgba_buf.chunks_exact_mut(4)) { - dst[0] = src[2]; - dst[1] = src[1]; - dst[2] = src[0]; - dst[3] = 255; - } - vec_to_rgba_image(width, height, rgba_buf) -} - -/// Some platforms e.g. MacOS can have extra bytes at the end of each row. -/// -/// See -/// https://github.com/nashaofu/xcap/issues/29 -/// https://github.com/nashaofu/xcap/issues/38 -#[cfg(any(target_os = "macos", test))] -pub fn remove_extra_data( - width: usize, - height: usize, - bytes_per_row: usize, - buf: Vec, -) -> Vec { - let extra_bytes_per_row = bytes_per_row - width * 4; - let mut result = Vec::with_capacity(buf.len() - extra_bytes_per_row * height); - for row in buf.chunks_exact(bytes_per_row) { - result.extend_from_slice(&row[..width * 4]); - } - - result -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn bgra() { - let image = bgra_to_rgba_image(2, 1, vec![1, 2, 3, 255, 255, 254, 253, 255]).unwrap(); - assert_eq!( - image, - RgbaImage::from_vec(2, 1, vec![3, 2, 1, 255, 253, 254, 255, 255]).unwrap() - ); - } - - #[test] - fn extra_data() { - let clean = remove_extra_data( - 2, - 2, - 9, - vec![ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, - ], - ); - assert_eq!( - clean, - vec![1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18] - ); - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs deleted file mode 100644 index 14995d4..0000000 --- a/src/utils/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod image; diff --git a/src/windows/boxed.rs b/src/windows/boxed.rs index 45190c0..07df664 100644 --- a/src/windows/boxed.rs +++ b/src/windows/boxed.rs @@ -1,12 +1,17 @@ +use log::error; use std::{ops::Deref, ptr}; use windows::{ core::PCWSTR, Win32::{ - Foundation::HWND, + Foundation::{CloseHandle, HANDLE, HWND}, Graphics::Gdi::{CreateDCW, DeleteDC, DeleteObject, GetWindowDC, ReleaseDC, HBITMAP, HDC}, + System::Threading::{OpenProcess, PROCESS_ACCESS_RIGHTS}, }, }; +use crate::XCapResult; + +#[derive(Debug)] pub(super) struct BoxHDC { hdc: HDC, hwnd: Option, @@ -25,9 +30,13 @@ impl Drop for BoxHDC { // https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-releasedc unsafe { if let Some(hwnd) = self.hwnd { - ReleaseDC(hwnd, self.hdc); + if ReleaseDC(hwnd, self.hdc) != 1 { + error!("ReleaseDC {:?} failed", self) + } } else { - DeleteDC(self.hdc); + if !DeleteDC(self.hdc).as_bool() { + error!("DeleteDC {:?} failed", self) + } } }; } @@ -66,6 +75,7 @@ impl From for BoxHDC { } } +#[derive(Debug)] pub(super) struct BoxHBITMAP(HBITMAP); impl Deref for BoxHBITMAP { @@ -79,7 +89,9 @@ impl Drop for BoxHBITMAP { fn drop(&mut self) { // https://learn.microsoft.com/zh-cn/windows/win32/api/wingdi/nf-wingdi-createcompatiblebitmap unsafe { - DeleteObject(self.0); + if !DeleteObject(self.0).as_bool() { + error!("DeleteObject {:?} failed", self) + } }; } } @@ -89,3 +101,33 @@ impl BoxHBITMAP { BoxHBITMAP(h_bitmap) } } + +#[derive(Debug)] +pub(super) struct BoxProcessHandle(HANDLE); + +impl Deref for BoxProcessHandle { + type Target = HANDLE; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Drop for BoxProcessHandle { + fn drop(&mut self) { + unsafe { + CloseHandle(self.0).unwrap_or_else(|_| error!("CloseHandle {:?} failed", self)); + }; + } +} + +impl BoxProcessHandle { + pub fn open( + dw_desired_access: PROCESS_ACCESS_RIGHTS, + b_inherit_handle: bool, + dw_process_id: u32, + ) -> XCapResult { + let h_process = unsafe { OpenProcess(dw_desired_access, b_inherit_handle, dw_process_id)? }; + + Ok(BoxProcessHandle(h_process)) + } +} diff --git a/src/windows/capture.rs b/src/windows/capture.rs index 991f431..60c7461 100644 --- a/src/windows/capture.rs +++ b/src/windows/capture.rs @@ -10,10 +10,7 @@ use windows::Win32::{ UI::WindowsAndMessaging::{GetDesktopWindow, PW_RENDERFULLCONTENT}, }; -use crate::{ - error::{XCapError, XCapResult}, - utils::image::bgra_to_rgba_image, -}; +use crate::error::{XCapError, XCapResult}; use super::boxed::{BoxHBITMAP, BoxHDC}; @@ -40,8 +37,7 @@ fn to_rgba_image( bmiColors: [RGBQUAD::default(); 1], }; - let data = vec![0u8; (width * height) as usize * 4]; - let buf_prt = data.as_ptr() as *mut _; + let mut buffer = vec![0u8; (width * height) as usize * 4]; unsafe { // 读取数据到 buffer 中 @@ -50,7 +46,7 @@ fn to_rgba_image( *box_h_bitmap, 0, height as u32, - Some(buf_prt), + Some(buffer.as_mut_ptr().cast()), &mut bitmap_info, DIB_RGB_COLORS, ) == 0; @@ -60,7 +56,16 @@ fn to_rgba_image( } }; - bgra_to_rgba_image(width as u32, height as u32, data) + for src in buffer.chunks_exact_mut(4) { + src.swap(0, 2); + // fix https://github.com/nashaofu/xcap/issues/92#issuecomment-1910014951 + if src[3] == 0 { + src[3] = 255; + } + } + + RgbaImage::from_raw(width as u32, height as u32, buffer) + .ok_or_else(|| XCapError::new("RgbaImage::from_raw failed")) } #[allow(unused)] diff --git a/src/windows/impl_window.rs b/src/windows/impl_window.rs index e052a4d..d504eb8 100644 --- a/src/windows/impl_window.rs +++ b/src/windows/impl_window.rs @@ -1,19 +1,29 @@ +use core::slice; 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 std::{ffi::c_void, mem, ptr}; +use windows::{ + core::{HSTRING, PCWSTR}, + Win32::{ + Foundation::{BOOL, HMODULE, HWND, LPARAM, MAX_PATH, TRUE}, + Graphics::{ + Dwm::{DwmGetWindowAttribute, DWMWA_CLOAKED, DWM_CLOAKED_SHELL}, + Gdi::{MonitorFromWindow, MONITOR_DEFAULTTONEAREST}, + }, + Storage::FileSystem::{GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW}, + System::{ + ProcessStatus::{GetModuleBaseNameW, GetModuleFileNameExW}, + Threading::PROCESS_QUERY_LIMITED_INFORMATION, + }, + UI::WindowsAndMessaging::{ + EnumWindows, GetAncestor, GetLastActivePopup, GetWindowInfo, GetWindowLongW, + GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, IsIconic, + IsWindowVisible, IsZoomed, GA_ROOTOWNER, GWL_EXSTYLE, WINDOWINFO, WINDOW_EX_STYLE, + WS_EX_TOOLWINDOW, + }, }, }; -use crate::error::XCapResult; +use crate::{error::XCapResult, platform::boxed::BoxProcessHandle}; use super::{capture::capture_window, impl_monitor::ImplMonitor, utils::wide_string_to_string}; @@ -103,6 +113,104 @@ unsafe extern "system" fn enum_windows_proc(hwnd: HWND, state: LPARAM) -> BOOL { TRUE } +#[derive(Debug, Default)] +struct LangCodePage { + pub w_language: u16, + pub w_code_page: u16, +} + +fn get_app_name(hwnd: HWND) -> XCapResult { + unsafe { + let mut lp_dw_process_id = 0; + GetWindowThreadProcessId(hwnd, Some(&mut lp_dw_process_id)); + + let box_process_handle = + BoxProcessHandle::open(PROCESS_QUERY_LIMITED_INFORMATION, false, lp_dw_process_id)?; + + let mut filename = [0; MAX_PATH as usize]; + GetModuleFileNameExW(*box_process_handle, HMODULE::default(), &mut filename); + + let pcw_filename = PCWSTR::from_raw(filename.as_ptr()); + + let file_version_info_size_w = GetFileVersionInfoSizeW(pcw_filename, None); + + let mut file_version_info = vec![0u16; file_version_info_size_w as usize]; + + GetFileVersionInfoW( + pcw_filename, + 0, + file_version_info_size_w, + file_version_info.as_mut_ptr().cast(), + )?; + + let mut lang_code_pages_ptr = ptr::null_mut(); + let mut lang_code_pages_length = 0; + + VerQueryValueW( + file_version_info.as_ptr().cast(), + &HSTRING::from("\\VarFileInfo\\Translation"), + &mut lang_code_pages_ptr, + &mut lang_code_pages_length, + ) + .ok()?; + + let lang_code_pages: &[LangCodePage] = + slice::from_raw_parts(lang_code_pages_ptr.cast(), lang_code_pages_length as usize); + + // 按照 keys 的顺序读取文件的属性值 + // 优先读取 ProductName + let keys = [ + "ProductName", + "FileDescription", + "ProductShortName", + "InternalName", + "OriginalFilename", + ]; + + for key in keys { + for lang_code_page in lang_code_pages { + let query_key = HSTRING::from(format!( + "\\StringFileInfo\\{:04x}{:04x}\\{}", + lang_code_page.w_language, lang_code_page.w_code_page, key + )); + + let mut value_ptr = ptr::null_mut(); + let mut value_length: u32 = 0; + + let is_success = VerQueryValueW( + file_version_info.as_ptr().cast(), + &query_key, + &mut value_ptr, + &mut value_length, + ) + .as_bool(); + + if !is_success { + continue; + } + + let value = slice::from_raw_parts(value_ptr.cast(), value_length as usize); + let attr = wide_string_to_string(value)?; + let attr = attr.trim(); + + if !attr.trim().is_empty() { + return Ok(attr.to_string()); + } + } + } + + // 默认使用 module_basename + let mut module_base_name_w = [0; MAX_PATH as usize]; + GetModuleBaseNameW( + *box_process_handle, + HMODULE::default(), + &mut module_base_name_w, + ); + + wide_string_to_string(&module_base_name_w) + } +} + impl ImplWindow { fn new(hwnd: HWND) -> XCapResult { unsafe { @@ -118,6 +226,8 @@ impl ImplWindow { wide_string_to_string(&wide_buffer)? }; + let app_name = get_app_name(hwnd)?; + let hmonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); let rc_client = window_info.rcClient; let is_minimized = IsIconic(hwnd).as_bool(); @@ -128,7 +238,7 @@ impl ImplWindow { window_info, id: hwnd.0 as u32, title, - app_name: String::from("Unsupported"), + app_name, current_monitor: ImplMonitor::new(hmonitor)?, x: rc_client.left, y: rc_client.top, diff --git a/src/windows/utils.rs b/src/windows/utils.rs index 9649e69..8a22c98 100644 --- a/src/windows/utils.rs +++ b/src/windows/utils.rs @@ -1,10 +1,11 @@ -use crate::error::{XCapError, XCapResult}; +use crate::error::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); - } + let string = if let Some(null_pos) = wide_string.iter().position(|pos| *pos == 0) { + String::from_utf16(&wide_string[..null_pos])? + } else { + String::from_utf16(&wide_string)? + }; - Err(XCapError::new("Convert wide string to string failed")) + Ok(string) }