425 lines
13 KiB
Rust
425 lines
13 KiB
Rust
pub type ScreenResult<T> = Result<T, anyhow::Error>;
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub struct Pixel {
|
|
pub a: u8,
|
|
pub r: u8,
|
|
pub g: u8,
|
|
pub b: u8,
|
|
}
|
|
|
|
pub struct Screenshot {
|
|
pub data: Vec<Pixel>,
|
|
}
|
|
|
|
impl Screenshot {
|
|
pub fn average_color(&self, brightness_boost: f32, saturation_boost: f32) -> rgb::Rgb<u8> {
|
|
let mut sum_l = 0.0f32;
|
|
let mut sum_a = 0.0f32;
|
|
let mut sum_b = 0.0f32;
|
|
|
|
for pixel in &self.data {
|
|
if pixel.a == 0 {
|
|
continue;
|
|
}
|
|
|
|
let r = pixel.r as f32 / 255.0;
|
|
let g = pixel.g as f32 / 255.0;
|
|
let b = pixel.b as f32 / 255.0;
|
|
|
|
let linear_rgb = oklab::LinearRgb::new(r, g, b);
|
|
let oklab_color = oklab::linear_srgb_to_oklab(linear_rgb);
|
|
sum_l += oklab_color.l;
|
|
sum_a += oklab_color.a;
|
|
sum_b += oklab_color.b;
|
|
}
|
|
|
|
let count = self.data.len() as f32;
|
|
let avg_l = sum_l / count;
|
|
let avg_a = sum_a / count;
|
|
let avg_b = sum_b / count;
|
|
|
|
let target_l = brightness_boost;
|
|
let boosted_l = (avg_l + target_l) / 2.0;
|
|
|
|
let boosted_a = avg_a * saturation_boost;
|
|
let boosted_b = avg_b * saturation_boost;
|
|
|
|
let oklab_avg = oklab::Oklab {
|
|
l: boosted_l,
|
|
a: boosted_a,
|
|
b: boosted_b,
|
|
};
|
|
let linear_rgb = oklab::oklab_to_linear_srgb(oklab_avg);
|
|
|
|
rgb::Rgb {
|
|
r: (linear_rgb.r * 255.0).clamp(0.0, 255.0) as u8,
|
|
g: (linear_rgb.g * 255.0).clamp(0.0, 255.0) as u8,
|
|
b: (linear_rgb.b * 255.0).clamp(0.0, 255.0) as u8,
|
|
}
|
|
}
|
|
|
|
pub fn color_diff(
|
|
&self,
|
|
color: &rgb::Rgb<u8>,
|
|
brightness_boost: f32,
|
|
saturation_boost: f32,
|
|
) -> f32 {
|
|
let avg_color = self.average_color(brightness_boost, saturation_boost);
|
|
|
|
let r1 = avg_color.r as f32 / 255.0;
|
|
let g1 = avg_color.g as f32 / 255.0;
|
|
let b1 = avg_color.b as f32 / 255.0;
|
|
|
|
let r2 = color.r as f32 / 255.0;
|
|
let g2 = color.g as f32 / 255.0;
|
|
let b2 = color.b as f32 / 255.0;
|
|
|
|
let oklab1 = oklab::linear_srgb_to_oklab(oklab::LinearRgb::new(r1, g1, b1));
|
|
let oklab2 = oklab::linear_srgb_to_oklab(oklab::LinearRgb::new(r2, g2, b2));
|
|
|
|
let delta_l = (oklab1.l - oklab2.l).abs();
|
|
let delta_a = (oklab1.a - oklab2.a).abs();
|
|
let delta_b = (oklab1.b - oklab2.b).abs();
|
|
|
|
let delta_e = (delta_l * delta_l + delta_a * delta_a + delta_b * delta_b).sqrt();
|
|
|
|
(delta_e * 1000.0).clamp(0.0, 100.0) as f32
|
|
}
|
|
}
|
|
|
|
#[cfg(any(
|
|
target_os = "linux",
|
|
not(any(target_os = "macos", target_os = "windows"))
|
|
))]
|
|
mod ffi {
|
|
use super::{Pixel, ScreenResult, Screenshot};
|
|
use grim_rs::Grim;
|
|
|
|
pub fn get_screenshot_inner(_screen: usize) -> ScreenResult<Screenshot> {
|
|
let mut grim =
|
|
Grim::new().map_err(|e| anyhow::anyhow!("Failed to initialize grim: {}", e))?;
|
|
let result = grim
|
|
.capture_all()
|
|
.map_err(|e| anyhow::anyhow!("Failed to capture screenshot: {}", e))?;
|
|
|
|
let raw_data = result.data();
|
|
|
|
let pixels: Vec<Pixel> = raw_data
|
|
.chunks_exact(4)
|
|
.map(|chunk| Pixel {
|
|
r: chunk[0],
|
|
g: chunk[1],
|
|
b: chunk[2],
|
|
a: chunk[3],
|
|
})
|
|
.collect();
|
|
|
|
Ok(Screenshot { data: pixels })
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
mod ffi {
|
|
#![allow(non_upper_case_globals, dead_code)]
|
|
use super::{Pixel, ScreenResult, Screenshot};
|
|
use libc;
|
|
use std::slice;
|
|
|
|
type CFIndex = libc::c_long;
|
|
type CFDataRef = *const u8;
|
|
type CGError = libc::int32_t;
|
|
type CGDirectDisplayID = libc::uint32_t;
|
|
type CGDisplayCount = libc::uint32_t;
|
|
type CGImageRef = *mut u8;
|
|
type CGDataProviderRef = *mut u8;
|
|
const CGDisplayNoErr: CGError = 0;
|
|
|
|
#[link(name = "CoreGraphics", kind = "framework")]
|
|
extern "C" {
|
|
fn CGGetActiveDisplayList(
|
|
max_displays: libc::uint32_t,
|
|
active_displays: *mut CGDirectDisplayID,
|
|
display_count: *mut CGDisplayCount,
|
|
) -> CGError;
|
|
fn CGDisplayCreateImage(displayID: CGDirectDisplayID) -> CGImageRef;
|
|
fn CGImageRelease(image: CGImageRef);
|
|
fn CGImageGetBitsPerPixel(image: CGImageRef) -> libc::size_t;
|
|
fn CGImageGetDataProvider(image: CGImageRef) -> CGDataProviderRef;
|
|
fn CGImageGetHeight(image: CGImageRef) -> libc::size_t;
|
|
fn CGImageGetWidth(image: CGImageRef) -> libc::size_t;
|
|
fn CGDataProviderCopyData(provider: CGDataProviderRef) -> CFDataRef;
|
|
}
|
|
|
|
#[link(name = "CoreFoundation", kind = "framework")]
|
|
extern "C" {
|
|
fn CFDataGetLength(theData: CFDataRef) -> CFIndex;
|
|
fn CFDataGetBytePtr(theData: CFDataRef) -> *const u8;
|
|
fn CFRelease(cf: *const libc::c_void);
|
|
}
|
|
|
|
pub fn get_screenshot_inner(screen: usize) -> ScreenResult<Screenshot> {
|
|
unsafe {
|
|
let mut count: CGDisplayCount = 0;
|
|
let mut err = CGGetActiveDisplayList(0, std::ptr::null_mut(), &mut count);
|
|
if err != CGDisplayNoErr {
|
|
return Err(anyhow::anyhow!("Error getting number of displays."));
|
|
}
|
|
|
|
let mut disps: Vec<CGDisplayCount> = Vec::with_capacity(count as usize);
|
|
disps.set_len(count as usize);
|
|
err = CGGetActiveDisplayList(
|
|
disps.len() as libc::uint32_t,
|
|
disps.as_mut_ptr(),
|
|
&mut count,
|
|
);
|
|
if err != CGDisplayNoErr {
|
|
return Err(anyhow::anyhow!("Error getting list of displays."));
|
|
}
|
|
|
|
let disp_id = disps[screen];
|
|
let cg_img = CGDisplayCreateImage(disp_id);
|
|
let width = CGImageGetWidth(cg_img) as usize;
|
|
let height = CGImageGetHeight(cg_img) as usize;
|
|
let pixel_bits = CGImageGetBitsPerPixel(cg_img) as usize;
|
|
|
|
if pixel_bits % 8 != 0 {
|
|
return Err(anyhow::anyhow!("Pixels aren't integral bytes."));
|
|
}
|
|
|
|
let cf_data = CGDataProviderCopyData(CGImageGetDataProvider(cg_img));
|
|
let raw_len = CFDataGetLength(cf_data) as usize;
|
|
|
|
let data = slice::from_raw_parts(CFDataGetBytePtr(cf_data), raw_len);
|
|
let pixels: Vec<Pixel> = data
|
|
.chunks_exact(4)
|
|
.map(|chunk| Pixel {
|
|
b: chunk[0],
|
|
g: chunk[1],
|
|
r: chunk[2],
|
|
a: chunk[3],
|
|
})
|
|
.collect();
|
|
|
|
CGImageRelease(cg_img);
|
|
CFRelease(cf_data as *const libc::c_void);
|
|
|
|
Ok(Screenshot { data: pixels })
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
mod ffi {
|
|
#![allow(non_snake_case, dead_code)]
|
|
use super::{Pixel, ScreenResult, Screenshot};
|
|
use std::ffi::c_void;
|
|
use std::mem::size_of;
|
|
use std::os::raw::{c_int, c_long, c_uint};
|
|
|
|
type PVOID = *mut c_void;
|
|
type LPVOID = *mut c_void;
|
|
type WORD = u16;
|
|
type DWORD = u32;
|
|
type BOOL = c_int;
|
|
type BYTE = u8;
|
|
type UINT = c_uint;
|
|
type LONG = c_long;
|
|
type LPARAM = c_long;
|
|
|
|
#[repr(C)]
|
|
struct RECT {
|
|
left: LONG,
|
|
top: LONG,
|
|
right: LONG,
|
|
bottom: LONG,
|
|
}
|
|
type LPCRECT = *const RECT;
|
|
type LPRECT = *mut RECT;
|
|
|
|
type HANDLE = PVOID;
|
|
type HMONITOR = HANDLE;
|
|
type HWND = HANDLE;
|
|
type HDC = HANDLE;
|
|
|
|
type HBITMAP = HANDLE;
|
|
type HGDIOBJ = HANDLE;
|
|
type LPBITMAPINFO = PVOID;
|
|
|
|
const NULL: *mut c_void = 0usize as *mut c_void;
|
|
const HGDI_ERROR: *mut c_void = -1isize as *mut c_void;
|
|
const SM_CXSCREEN: c_int = 0;
|
|
const SM_CYSCREEN: c_int = 1;
|
|
|
|
const SRCCOPY: u32 = 0x00CC0020;
|
|
const CAPTUREBLT: u32 = 0x40000000;
|
|
const DIB_RGB_COLORS: UINT = 0;
|
|
const BI_RGB: DWORD = 0;
|
|
|
|
#[repr(C)]
|
|
struct BITMAPINFOHEADER {
|
|
biSize: DWORD,
|
|
biWidth: LONG,
|
|
biHeight: LONG,
|
|
biPlanes: WORD,
|
|
biBitCount: WORD,
|
|
biCompression: DWORD,
|
|
biSizeImage: DWORD,
|
|
biXPelsPerMeter: LONG,
|
|
biYPelsPerMeter: LONG,
|
|
biClrUsed: DWORD,
|
|
biClrImportant: DWORD,
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct RGBQUAD {
|
|
rgbBlue: BYTE,
|
|
rgbGreen: BYTE,
|
|
rgbRed: BYTE,
|
|
rgbReserved: BYTE,
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct BITMAPINFO {
|
|
bmiHeader: BITMAPINFOHEADER,
|
|
bmiColors: [RGBQUAD; 1],
|
|
}
|
|
|
|
#[link(name = "user32")]
|
|
extern "system" {
|
|
fn GetSystemMetrics(m: c_int) -> c_int;
|
|
fn GetDesktopWindow() -> HWND;
|
|
fn GetDC(hWnd: HWND) -> HDC;
|
|
}
|
|
|
|
#[link(name = "gdi32")]
|
|
extern "system" {
|
|
fn CreateCompatibleDC(hdc: HDC) -> HDC;
|
|
fn CreateCompatibleBitmap(hdc: HDC, nWidth: c_int, nHeight: c_int) -> HBITMAP;
|
|
fn SelectObject(hdc: HDC, hgdiobj: HGDIOBJ) -> HGDIOBJ;
|
|
fn BitBlt(
|
|
hdcDest: HDC,
|
|
nXDest: c_int,
|
|
nYDest: c_int,
|
|
nWidth: c_int,
|
|
nHeight: c_int,
|
|
hdcSrc: HDC,
|
|
nXSrc: c_int,
|
|
nYSrc: c_int,
|
|
dwRop: DWORD,
|
|
) -> BOOL;
|
|
fn GetDIBits(
|
|
hdc: HDC,
|
|
hbmp: HBITMAP,
|
|
uStartScan: UINT,
|
|
cScanLines: UINT,
|
|
lpvBits: LPVOID,
|
|
lpbi: LPBITMAPINFO,
|
|
uUsage: UINT,
|
|
) -> c_int;
|
|
fn DeleteObject(hObject: HGDIOBJ) -> BOOL;
|
|
fn ReleaseDC(hWnd: HWND, hDC: HDC) -> c_int;
|
|
fn DeleteDC(hdc: HDC) -> BOOL;
|
|
}
|
|
|
|
pub fn get_screenshot_inner(_screen: usize) -> ScreenResult<Screenshot> {
|
|
unsafe {
|
|
let h_wnd_screen = GetDesktopWindow();
|
|
let h_dc_screen = GetDC(h_wnd_screen);
|
|
let width = GetSystemMetrics(SM_CXSCREEN);
|
|
let height = GetSystemMetrics(SM_CYSCREEN);
|
|
|
|
let h_dc = CreateCompatibleDC(h_dc_screen);
|
|
if h_dc == NULL {
|
|
return Err(anyhow::anyhow!("Can't get a Windows display."));
|
|
}
|
|
|
|
let h_bmp = CreateCompatibleBitmap(h_dc_screen, width, height);
|
|
if h_bmp == NULL {
|
|
return Err(anyhow::anyhow!("Can't create a Windows buffer"));
|
|
}
|
|
|
|
let res = SelectObject(h_dc, h_bmp);
|
|
if res == NULL || res == HGDI_ERROR {
|
|
return Err(anyhow::anyhow!("Can't select Windows buffer."));
|
|
}
|
|
|
|
let res = BitBlt(
|
|
h_dc,
|
|
0,
|
|
0,
|
|
width,
|
|
height,
|
|
h_dc_screen,
|
|
0,
|
|
0,
|
|
SRCCOPY | CAPTUREBLT,
|
|
);
|
|
if res == 0 {
|
|
return Err(anyhow::anyhow!("Failed to copy screen to Windows buffer"));
|
|
}
|
|
|
|
let pixel_width: usize = 4;
|
|
let mut bmi = BITMAPINFO {
|
|
bmiHeader: BITMAPINFOHEADER {
|
|
biSize: size_of::<BITMAPINFOHEADER>() as DWORD,
|
|
biWidth: width as LONG,
|
|
biHeight: height as LONG,
|
|
biPlanes: 1,
|
|
biBitCount: 8 * pixel_width as WORD,
|
|
biCompression: BI_RGB,
|
|
biSizeImage: (width * height * pixel_width as c_int) as DWORD,
|
|
biXPelsPerMeter: 0,
|
|
biYPelsPerMeter: 0,
|
|
biClrUsed: 0,
|
|
biClrImportant: 0,
|
|
},
|
|
bmiColors: [RGBQUAD {
|
|
rgbBlue: 0,
|
|
rgbGreen: 0,
|
|
rgbRed: 0,
|
|
rgbReserved: 0,
|
|
}],
|
|
};
|
|
|
|
let size: usize = (width * height) as usize * pixel_width;
|
|
let mut data: Vec<u8> = vec![0; size];
|
|
|
|
GetDIBits(
|
|
h_dc,
|
|
h_bmp,
|
|
0,
|
|
height as DWORD,
|
|
data.as_mut_ptr() as *mut c_void,
|
|
&mut bmi as *mut BITMAPINFO as *mut c_void,
|
|
DIB_RGB_COLORS,
|
|
);
|
|
|
|
ReleaseDC(h_wnd_screen, h_dc_screen);
|
|
DeleteDC(h_dc);
|
|
DeleteObject(h_bmp);
|
|
|
|
let row_len = width as usize * pixel_width;
|
|
let mut pixels: Vec<Pixel> = Vec::with_capacity((width * height) as usize);
|
|
|
|
for row_i in (0..height as usize).rev() {
|
|
for byte_i in (0..width as usize) {
|
|
let idx = row_i * row_len + byte_i * pixel_width;
|
|
pixels.push(Pixel {
|
|
b: data[idx],
|
|
g: data[idx + 1],
|
|
r: data[idx + 2],
|
|
a: data[idx + 3],
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(Screenshot { data: pixels })
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_screenshot(screen: usize) -> ScreenResult<Screenshot> {
|
|
ffi::get_screenshot_inner(screen)
|
|
}
|