use crate::capture::{Capture, ScreenshotsCapture};
use crate::color::{self, ColorBlob, ColorSpec};
use crate::frame::{Frame, Rect};
use crate::matcher::{Match, Matcher, RgbMatcher};
use crate::template::Template;
use crate::Result;
use std::time::{Duration, Instant};
#[cfg(all(windows, feature = "capture-dxgi"))]
use crate::Error;
#[cfg(feature = "match-corr")]
use crate::matcher_corr::{CorrConfig, CorrMatcher};
pub enum CaptureKind {
Screenshots,
#[cfg(all(windows, feature = "capture-gdi"))]
Gdi,
#[cfg(all(windows, feature = "capture-dxgi"))]
Dxgi,
#[cfg(all(windows, feature = "capture-window"))]
Window(crate::capture_window::WindowHandle),
#[cfg(all(windows, any(feature = "capture-gdi", feature = "capture-dxgi")))]
Auto,
}
pub enum MatchKind {
Rgb { tolerance: i32 },
#[cfg(feature = "match-corr")]
Corr,
#[cfg(feature = "match-corr")]
CorrWith(CorrConfig),
}
pub struct Finder {
capture: Box<dyn Capture>,
matcher: Box<dyn Matcher>,
region: Option<Rect>,
frame: Frame,
cache_key: Option<u64>,
cache_result: Option<Option<Match>>,
}
impl Finder {
pub fn builder() -> FinderBuilder {
FinderBuilder {
capture: None,
matcher: None,
region: None,
}
}
pub fn new(capture: Box<dyn Capture>, matcher: Box<dyn Matcher>) -> Self {
Finder {
capture,
matcher,
region: None,
frame: Frame::bgra8(0, 0, Vec::new()),
cache_key: None,
cache_result: None,
}
}
pub fn find_on_screen(&mut self, tpl: &Template) -> Result<Option<Match>> {
let _t0 = px_timer!();
let changed = self.capture.grab_into(&mut self.frame)?;
let key = self.key_of(tpl);
if !changed && self.cache_key == Some(key) {
if let Some(prev) = self.cache_result {
px_trace!(
op = "find_on_screen",
backend = self.capture.backend(),
cache_hit = true,
hit = prev.is_some(),
elapsed_us = _t0.map(|t| t.elapsed().as_micros() as u64).unwrap_or(0),
);
return Ok(prev);
}
}
let m = self.find_in_frame(&self.frame, tpl);
self.cache_key = Some(key);
self.cache_result = Some(m);
px_trace!(
op = "find_on_screen",
backend = self.capture.backend(),
cache_hit = false,
changed,
frame = format!("{}x{}", self.frame.width, self.frame.height),
tpl_key = format!("{:016x}", key),
hit = m.is_some(),
elapsed_us = _t0.map(|t| t.elapsed().as_micros() as u64).unwrap_or(0),
);
Ok(m)
}
pub fn find_color_on_screen(
&mut self,
spec: &ColorSpec,
min_area: usize,
) -> Result<Vec<ColorBlob>> {
let _t0 = px_timer!();
self.capture.grab_into(&mut self.frame)?;
let region = self.region.unwrap_or_else(|| self.frame.full_rect());
let blobs = color::find_blobs(&self.frame, spec, region, min_area);
px_trace!(
op = "find_color_on_screen",
backend = self.capture.backend(),
color = format!("{:?}±{}", spec.rgb, spec.tolerance),
region = format!(
"({},{},{}x{})",
region.x, region.y, region.width, region.height
),
blobs = blobs.len(),
elapsed_us = _t0.map(|t| t.elapsed().as_micros() as u64).unwrap_or(0),
);
Ok(blobs)
}
pub fn find_until(
&mut self,
tpl: &Template,
timeout: Duration,
interval: Duration,
) -> Result<Option<Match>> {
let deadline = Instant::now() + timeout;
loop {
if let Some(m) = self.find_on_screen(tpl)? {
px_trace!(op = "find_until", result = "hit", timeout = ?timeout);
return Ok(Some(m));
}
let now = Instant::now();
if now >= deadline {
px_trace!(op = "find_until", result = "timeout", timeout = ?timeout);
return Ok(None);
}
std::thread::sleep(interval.min(deadline - now));
}
}
pub fn wait_gone(
&mut self,
tpl: &Template,
timeout: Duration,
interval: Duration,
) -> Result<bool> {
let deadline = Instant::now() + timeout;
loop {
if self.find_on_screen(tpl)?.is_none() {
px_trace!(op = "wait_gone", result = "gone", timeout = ?timeout);
return Ok(true);
}
let now = Instant::now();
if now >= deadline {
px_trace!(op = "wait_gone", result = "still_present", timeout = ?timeout);
return Ok(false);
}
std::thread::sleep(interval.min(deadline - now));
}
}
pub fn find_all_on_screen(&mut self, tpl: &Template, max: usize) -> Result<Vec<Match>> {
let changed = self.capture.grab_into(&mut self.frame)?;
let _ = changed;
let region = self.region.unwrap_or_else(|| self.frame.full_rect());
Ok(self.matcher.find_all(&self.frame, tpl, region, max))
}
pub fn find_many_on_screen(&mut self, tpls: &[&Template]) -> Result<Vec<Option<Match>>> {
self.capture.grab_into(&mut self.frame)?;
Ok(tpls
.iter()
.map(|t| self.find_in_frame(&self.frame, t))
.collect())
}
pub fn find_in_frame(&self, frame: &Frame, tpl: &Template) -> Option<Match> {
match self.region {
Some(r) => self.matcher.find_in(frame, tpl, r),
None => self.matcher.find(frame, tpl),
}
}
pub fn region(&self) -> Option<Rect> {
self.region
}
pub fn set_region(&mut self, region: Option<Rect>) {
self.region = region;
self.cache_key = None;
self.cache_result = None;
}
fn key_of(&self, tpl: &Template) -> u64 {
let rk = match self.region {
None => 0u64,
Some(r) => {
(r.x as u64).wrapping_mul(1000003)
^ (r.y as u64).wrapping_mul(7349287)
^ (r.width as u64).wrapping_mul(911)
^ (r.height as u64)
}
};
tpl.content_key() ^ rk.rotate_left(32)
}
}
pub struct FinderBuilder {
capture: Option<CaptureKind>,
matcher: Option<MatchKind>,
region: Option<Rect>,
}
impl FinderBuilder {
pub fn capture(mut self, k: CaptureKind) -> Self {
self.capture = Some(k);
self
}
pub fn matcher(mut self, k: MatchKind) -> Self {
self.matcher = Some(k);
self
}
pub fn region(mut self, r: impl Into<Rect>) -> Self {
self.region = Some(r.into());
self
}
pub fn build(self) -> Result<Finder> {
let capture: Box<dyn Capture> = match self.capture.unwrap_or(CaptureKind::Screenshots) {
CaptureKind::Screenshots => Box::new(ScreenshotsCapture::primary()?),
#[cfg(all(windows, feature = "capture-gdi"))]
CaptureKind::Gdi => Box::new(crate::capture_gdi::GdiCapture::new_primary()),
#[cfg(all(windows, feature = "capture-dxgi"))]
CaptureKind::Dxgi => Box::new(
crate::capture_dxgi::DxgiCapture::new_primary()
.ok_or_else(|| Error::Capture("DXGI desktop duplication unavailable".into()))?,
),
#[cfg(all(windows, feature = "capture-window"))]
CaptureKind::Window(h) => Box::new(crate::capture_window::WindowCapture::new(h)),
#[cfg(all(windows, any(feature = "capture-gdi", feature = "capture-dxgi")))]
CaptureKind::Auto => auto_capture()?,
};
let matcher: Box<dyn Matcher> =
match self.matcher.unwrap_or(MatchKind::Rgb { tolerance: 25 }) {
MatchKind::Rgb { tolerance } => Box::new(RgbMatcher::new(tolerance)),
#[cfg(feature = "match-corr")]
MatchKind::Corr => Box::new(CorrMatcher::new()),
#[cfg(feature = "match-corr")]
MatchKind::CorrWith(cfg) => Box::new(CorrMatcher::with_config(cfg)),
};
let mut f = Finder::new(capture, matcher);
f.region = self.region;
Ok(f)
}
}
#[cfg(all(windows, any(feature = "capture-gdi", feature = "capture-dxgi")))]
fn auto_capture() -> Result<Box<dyn Capture>> {
#[cfg(feature = "capture-dxgi")]
if let Some(c) = crate::capture_dxgi::DxgiCapture::new_primary() {
return Ok(Box::new(c));
}
#[cfg(feature = "capture-gdi")]
{
return Ok(Box::new(crate::capture_gdi::GdiCapture::new_primary()));
}
#[allow(unreachable_code)]
Ok(Box::new(ScreenshotsCapture::primary()?))
}
#[cfg(test)]
mod tests {
use super::*;
const W: usize = 32;
const H: usize = 32;
fn frame(with_target: bool) -> Frame {
let mut px = vec![0u8; W * H * 4];
if with_target {
for y in 10..18 {
for x in 10..18 {
let i = (y * W + x) * 4;
px[i] = 255;
px[i + 1] = 0;
px[i + 2] = 0;
px[i + 3] = 255;
}
}
}
Frame::rgba8(W, H, px)
}
struct SeqCapture {
present: Vec<bool>,
grabs: usize,
}
impl Capture for SeqCapture {
fn grab(&mut self) -> Result<Frame> {
let i = self.grabs.min(self.present.len() - 1);
self.grabs += 1;
Ok(frame(self.present[i]))
}
fn backend(&self) -> &'static str {
"mock"
}
}
fn target_tpl() -> Template {
Template::from_rgb([255, 0, 0].repeat(8 * 8), 8, 8)
}
fn finder_with(present: Vec<bool>) -> Finder {
Finder::new(
Box::new(SeqCapture { present, grabs: 0 }),
Box::new(RgbMatcher::new(0)),
)
}
#[test]
fn find_until_hits_after_a_few_misses() {
let mut f = finder_with(vec![false, false, true]);
let m = f
.find_until(
&target_tpl(),
Duration::from_secs(5),
Duration::from_millis(2),
)
.expect("mock 不会出错")
.expect("第 3 帧应命中");
assert_eq!((m.x, m.y), (10, 10));
}
#[test]
fn find_until_times_out() {
let mut f = finder_with(vec![false]);
let m = f
.find_until(
&target_tpl(),
Duration::from_millis(30),
Duration::from_millis(5),
)
.expect("mock 不会出错");
assert!(m.is_none(), "一直找不到应在超时后返回 None");
}
#[test]
fn wait_gone_returns_true_and_false() {
let mut f = finder_with(vec![true, true, false]);
assert!(f
.wait_gone(
&target_tpl(),
Duration::from_secs(5),
Duration::from_millis(2)
)
.unwrap());
let mut f2 = finder_with(vec![true]);
assert!(!f2
.wait_gone(
&target_tpl(),
Duration::from_millis(30),
Duration::from_millis(5)
)
.unwrap());
}
#[test]
fn find_color_on_screen_high_level_entry() {
let mut f = finder_with(vec![true]);
let blobs = f
.find_color_on_screen(&ColorSpec::new(255, 0, 0, 10), 32)
.unwrap();
assert_eq!(blobs.len(), 1);
assert_eq!(blobs[0].bounds, Rect::new(10, 10, 8, 8));
assert_eq!(blobs[0].area, 64);
f.set_region(Some(Rect::new(0, 20, 32, 12)));
assert!(f
.find_color_on_screen(&ColorSpec::new(255, 0, 0, 10), 1)
.unwrap()
.is_empty());
}
}