deskbot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +12 -0
- data/CHANGELOG.md +5 -0
- data/Cargo.lock +966 -0
- data/Cargo.toml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +236 -0
- data/Rakefile +18 -0
- data/ext/deskbot/Cargo.toml +16 -0
- data/ext/deskbot/extconf.rb +6 -0
- data/ext/deskbot/src/alert.rs +5 -0
- data/ext/deskbot/src/bitmap.rs +268 -0
- data/ext/deskbot/src/keys.rs +38 -0
- data/ext/deskbot/src/lib.rs +49 -0
- data/ext/deskbot/src/mouse.rs +55 -0
- data/ext/deskbot/src/screen.rs +34 -0
- data/lib/deskbot/area.rb +12 -0
- data/lib/deskbot/bitmap.rb +89 -0
- data/lib/deskbot/color.rb +36 -0
- data/lib/deskbot/point.rb +10 -0
- data/lib/deskbot/providers/autopilot/bitmap.rb +21 -0
- data/lib/deskbot/providers/autopilot.rb +28 -0
- data/lib/deskbot/screen.rb +131 -0
- data/lib/deskbot/size.rb +10 -0
- data/lib/deskbot/types.rb +30 -0
- data/lib/deskbot/version.rb +5 -0
- data/lib/deskbot.rb +28 -0
- data/sig/deskbot.rbs +4 -0
- metadata +132 -0
data/Cargo.toml
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Nolan J Tait
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
# Deskbot
|
2
|
+
|
3
|
+
This is a dekstop automation library written using
|
4
|
+
[autopilot-rs](https://github.com/autopilot-rs) and
|
5
|
+
[rust extensions](https://bundler.io/blog/2023/01/31/rust-gem-skeleton.html)
|
6
|
+
for Ruby.
|
7
|
+
|
8
|
+
This library uses [dry-types](https://dry-rb.org/gems/dry-types/1.7/) so the
|
9
|
+
arguments should be well documented in
|
10
|
+
the `lib/deskbot/screen.rb` and `lib/deskbot/bitmap.rb` files
|
11
|
+
|
12
|
+
The default implementation of finding images with autopilot was unbearably slow,
|
13
|
+
so I've rewritten matching templates to use
|
14
|
+
[opencv](https://github.com/twistedfall/opencv-rust). So ensure you have opencv
|
15
|
+
[installed](https://github.com/twistedfall/opencv-rust/blob/master/INSTALL.md).
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Install the gem and add to the application's Gemfile by executing:
|
20
|
+
|
21
|
+
$ bundle add deskbot
|
22
|
+
|
23
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
24
|
+
|
25
|
+
$ gem install deskbot
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
To start you can require the gem in your script and initialize a screen:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
require "deskbot"
|
33
|
+
|
34
|
+
screen = Deskbot.screen
|
35
|
+
```
|
36
|
+
|
37
|
+
### Typing
|
38
|
+
|
39
|
+
Type something on the keyboard
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# Type SOMETHING at 60 words per minute
|
43
|
+
screen.type("something", flags: [:shift], wpm: 60.0, noise: 0.0)
|
44
|
+
```
|
45
|
+
|
46
|
+
You can also tap a key:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# Tap shift + a after a 1 second delay
|
50
|
+
screen.tap_key("a", flags: [:shift], delay_ms: 1.0)
|
51
|
+
```
|
52
|
+
|
53
|
+
And even more primitively you can toggle a key:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
# Press the "a" key down
|
57
|
+
screen.toggle_key("a", down: true)
|
58
|
+
```
|
59
|
+
|
60
|
+
### Alerts
|
61
|
+
|
62
|
+
You can make alerts
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
screen.alert("Hello")
|
66
|
+
```
|
67
|
+
|
68
|
+
### Mouse movement
|
69
|
+
|
70
|
+
You can teleport your mouse somewhere on the screen:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
# Teleport the mouse to coordinates x: 100, y: 100
|
74
|
+
screen.move_mouse(100, 100)
|
75
|
+
```
|
76
|
+
|
77
|
+
You can also move the mouse smoothly somewhere:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
# Move the mouse smoothly to x: 100, y: 100 over 2 seconds
|
81
|
+
screen.smooth_move_mouse(100, 100, duration: 2)
|
82
|
+
```
|
83
|
+
|
84
|
+
You can click:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# Left click
|
88
|
+
screen.click
|
89
|
+
|
90
|
+
# Right click
|
91
|
+
screen.click(:right)
|
92
|
+
```
|
93
|
+
|
94
|
+
Or even scroll:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
# Scroll 1 click up
|
98
|
+
screen.scroll
|
99
|
+
|
100
|
+
# Scroll 5 clicks up
|
101
|
+
screen.scroll(clicks: 5)
|
102
|
+
|
103
|
+
# Scroll 5 clicks down
|
104
|
+
screen.scroll(:down, clicks: 5)
|
105
|
+
```
|
106
|
+
|
107
|
+
### Screen introspection
|
108
|
+
|
109
|
+
You can query the color of a specific pixel:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
# Get the color of a specific pixel at x: 100, y: 100
|
113
|
+
color = screen.color_at(100, 100)
|
114
|
+
```
|
115
|
+
|
116
|
+
This returns a `Deskbot::Color` object with `red`, `green`, `blue` and `alpha`
|
117
|
+
attributes.
|
118
|
+
|
119
|
+
You can query the size of the screen:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
size = screen.size
|
123
|
+
scale = screen.scale
|
124
|
+
```
|
125
|
+
|
126
|
+
The size would be a `Deskbot::Size` with `width` and `height` attributes. The
|
127
|
+
scale would simply be a float.
|
128
|
+
|
129
|
+
You can query if a point is visible on the screen:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
screen.point_visible?(100, 100)
|
133
|
+
screen.area_visible?(x: 100, y: 100, width: 400, height: 400)
|
134
|
+
```
|
135
|
+
|
136
|
+
### Bitmaps
|
137
|
+
|
138
|
+
You can capture your screen:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
# We can capture the whole screen
|
142
|
+
bitmap = screen.capture
|
143
|
+
|
144
|
+
# Or we can capture part of our screen
|
145
|
+
bitmap = screen.capture_area(x: 100, y: 100, width: 400, height: 400)
|
146
|
+
```
|
147
|
+
|
148
|
+
This returns a `Deskbot::Bitmap` which you can use to find areas that match
|
149
|
+
images you provide.
|
150
|
+
|
151
|
+
You can query the bounds of the bitmap:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
bitmap.bounds
|
155
|
+
```
|
156
|
+
|
157
|
+
Which would return a `Deskbot::Area` with `x`, `y`, `width` and `height`
|
158
|
+
attributes.
|
159
|
+
|
160
|
+
We can check for the colors of points on the bitmap:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
bitmap.color_at(100, 100)
|
164
|
+
```
|
165
|
+
|
166
|
+
Which returns a `Deskbot::Color`.
|
167
|
+
|
168
|
+
### Comparing images
|
169
|
+
|
170
|
+
You can compare images to your bitmap with a given tolerance (optional):
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
bitmap.find("images/test.jpg")
|
174
|
+
bitmap.all("images/test.jpg", tolerance: 4.0)
|
175
|
+
```
|
176
|
+
|
177
|
+
`find` and `find_color` both return `Dry::Monad::Result` objects meaning you
|
178
|
+
need to unwrap their values:
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
result = bitmap.find("images/test.jpg")
|
182
|
+
point = result.success
|
183
|
+
```
|
184
|
+
|
185
|
+
They are also wrapped with an optional matcher so you can use a much nicer
|
186
|
+
syntax to grab their values:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
bitmap.find("images/test.jpg") do |on|
|
190
|
+
on.success do |point|
|
191
|
+
# Do something with the point, no need to call point.success
|
192
|
+
end
|
193
|
+
|
194
|
+
on.failure do
|
195
|
+
# Handle you error here
|
196
|
+
end
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
These return `Deskbot::Point` objects with `x` and `y` attributes.
|
201
|
+
|
202
|
+
You can ask higher level questions about the image such as:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
bitmap.eql?("images/test.jpg", tolerance: 2.0)
|
206
|
+
bitmap.count("images/test.jpg")
|
207
|
+
```
|
208
|
+
|
209
|
+
You can also query for positions of colors:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
bitmap.find_color([255, 255, 255, 0], tolerance: 0.0)
|
213
|
+
bitmap.all_color([255, 255, 255, 0])
|
214
|
+
```
|
215
|
+
|
216
|
+
These will return `Deskbot::Point` objects with `x` and `y` attributes.
|
217
|
+
|
218
|
+
## Development
|
219
|
+
|
220
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
221
|
+
Then, run `rake spec` to run the tests. You can also run `bin/console` for
|
222
|
+
an interactive prompt that will allow you to experiment.
|
223
|
+
|
224
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
225
|
+
To release a new version, update the version number in `version.rb`, and then
|
226
|
+
run `bundle exec rake release`, which will create a git tag for the version,
|
227
|
+
push git commits and the created tag, and push the `.gem` file
|
228
|
+
to [rubygems.org](https://rubygems.org).
|
229
|
+
|
230
|
+
## Contributing
|
231
|
+
|
232
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/nolantait/deskbot.
|
233
|
+
|
234
|
+
## License
|
235
|
+
|
236
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
require "rb_sys/extensiontask"
|
9
|
+
|
10
|
+
task build: :compile
|
11
|
+
|
12
|
+
GEMSPEC = Gem::Specification.load("deskbot.gemspec")
|
13
|
+
|
14
|
+
RbSys::ExtensionTask.new("deskbot", GEMSPEC) do |ext|
|
15
|
+
ext.lib_dir = "lib/deskbot"
|
16
|
+
end
|
17
|
+
|
18
|
+
task default: %i[compile spec]
|
@@ -0,0 +1,16 @@
|
|
1
|
+
[package]
|
2
|
+
name = "deskbot"
|
3
|
+
version = "0.1.0"
|
4
|
+
edition = "2021"
|
5
|
+
authors = ["Nolan J Tait <nolanjtait@gmail.com>"]
|
6
|
+
license = "MIT"
|
7
|
+
publish = false
|
8
|
+
|
9
|
+
[lib]
|
10
|
+
crate-type = ["cdylib"]
|
11
|
+
|
12
|
+
[dependencies]
|
13
|
+
opencv = { version = "0.91.3", features = ["clang-runtime"] }
|
14
|
+
magnus = { version = "0.6.2" }
|
15
|
+
autopilot = { version = "0.4.0" }
|
16
|
+
image = { version = "0.22.4" }
|
@@ -0,0 +1,268 @@
|
|
1
|
+
extern crate autopilot;
|
2
|
+
|
3
|
+
use image::open;
|
4
|
+
use magnus::exception;
|
5
|
+
use std::collections::HashMap;
|
6
|
+
|
7
|
+
extern crate opencv;
|
8
|
+
|
9
|
+
use opencv::{
|
10
|
+
core::{self, Mat, MatTraitConst, Rect, Scalar},
|
11
|
+
imgproc, Result,
|
12
|
+
};
|
13
|
+
|
14
|
+
#[magnus::wrap(class = "Deskbot::Providers::Autopilot::Bitmap")]
|
15
|
+
pub struct Bitmap(autopilot::bitmap::Bitmap);
|
16
|
+
|
17
|
+
impl Bitmap {
|
18
|
+
fn new(bitmap: autopilot::bitmap::Bitmap) -> Self {
|
19
|
+
Bitmap(bitmap)
|
20
|
+
}
|
21
|
+
|
22
|
+
pub fn save(&self, path: String) -> Result<bool, magnus::Error> {
|
23
|
+
match self.0.image.save(path) {
|
24
|
+
Ok(_) => Ok(true),
|
25
|
+
Err(_) => Err(magnus::Error::new(
|
26
|
+
exception::runtime_error(),
|
27
|
+
"Could not save the image",
|
28
|
+
)),
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
pub fn bounds(&self) -> HashMap<String, f64> {
|
33
|
+
let bounds = self.0.bounds();
|
34
|
+
|
35
|
+
HashMap::from([
|
36
|
+
("x".to_string(), bounds.origin.x),
|
37
|
+
("y".to_string(), bounds.origin.y),
|
38
|
+
("width".to_string(), bounds.size.width),
|
39
|
+
("height".to_string(), bounds.size.height),
|
40
|
+
])
|
41
|
+
}
|
42
|
+
|
43
|
+
pub fn get_pixel(&self, x: f64, y: f64) -> Vec<u8> {
|
44
|
+
let point = autopilot::geometry::Point::new(x, y);
|
45
|
+
let value = self.0.get_pixel(point).0;
|
46
|
+
// For some reason I have to convert to a Vec<u8>. Magnus doesn't like [u8; 4]
|
47
|
+
return value.to_vec();
|
48
|
+
}
|
49
|
+
|
50
|
+
pub fn find_color(
|
51
|
+
&self,
|
52
|
+
color: [u8; 4],
|
53
|
+
tolerance: Option<f64>,
|
54
|
+
) -> Option<HashMap<String, f64>> {
|
55
|
+
if let Some(found) = self.0.find_color(image::Rgba(color), tolerance, None, None) {
|
56
|
+
return Some(HashMap::from([
|
57
|
+
("x".to_string(), found.x),
|
58
|
+
("y".to_string(), found.y),
|
59
|
+
]));
|
60
|
+
}
|
61
|
+
None
|
62
|
+
}
|
63
|
+
|
64
|
+
pub fn find(&self, image_path: String, tolerance: Option<f64>) -> Option<HashMap<String, f64>> {
|
65
|
+
let src = match opencv::imgcodecs::imread(
|
66
|
+
"./tmp/last-screenshot.png",
|
67
|
+
opencv::imgcodecs::IMREAD_COLOR,
|
68
|
+
) {
|
69
|
+
Ok(src) => src,
|
70
|
+
Err(error) => panic!("Could not read the image: {}", error),
|
71
|
+
};
|
72
|
+
let templ = match opencv::imgcodecs::imread(&image_path, opencv::imgcodecs::IMREAD_COLOR) {
|
73
|
+
Ok(templ) => templ,
|
74
|
+
Err(error) => panic!("Could not read the image: {}", error),
|
75
|
+
};
|
76
|
+
|
77
|
+
// Create Mat for the result
|
78
|
+
let mut dst = Mat::default();
|
79
|
+
let mask = Mat::default();
|
80
|
+
|
81
|
+
// Perform template matching
|
82
|
+
match imgproc::match_template(
|
83
|
+
&src,
|
84
|
+
&templ,
|
85
|
+
&mut dst,
|
86
|
+
imgproc::TemplateMatchModes::TM_SQDIFF_NORMED.into(),
|
87
|
+
&mask,
|
88
|
+
) {
|
89
|
+
Ok(_) => {}
|
90
|
+
Err(error) => panic!("Could not match the template: {}", error),
|
91
|
+
}
|
92
|
+
|
93
|
+
// Find the location of the best match
|
94
|
+
let (mut min_val, mut max_val) = (0.0, 0.0);
|
95
|
+
let mut min_point = core::Point { x: 0, y: 0 };
|
96
|
+
let mut max_point = core::Point { x: 0, y: 0 };
|
97
|
+
|
98
|
+
match core::min_max_loc(
|
99
|
+
&dst,
|
100
|
+
Some(&mut min_val),
|
101
|
+
Some(&mut max_val),
|
102
|
+
Some(&mut min_point),
|
103
|
+
Some(&mut max_point),
|
104
|
+
&mask,
|
105
|
+
) {
|
106
|
+
Ok(_) => {}
|
107
|
+
Err(error) => panic!("Could not find the min max loc: {}", error),
|
108
|
+
}
|
109
|
+
|
110
|
+
if min_val > tolerance.unwrap_or(0.0) {
|
111
|
+
return None;
|
112
|
+
}
|
113
|
+
|
114
|
+
Some(HashMap::from([
|
115
|
+
("x".to_string(), min_point.x as f64),
|
116
|
+
("y".to_string(), min_point.y as f64),
|
117
|
+
]))
|
118
|
+
}
|
119
|
+
|
120
|
+
pub fn all_color(&self, color: [u8; 4], tolerance: Option<f64>) -> Vec<HashMap<String, f64>> {
|
121
|
+
let mut results = vec![];
|
122
|
+
for found in self
|
123
|
+
.0
|
124
|
+
.find_every_color(image::Rgba(color), tolerance, None, None)
|
125
|
+
{
|
126
|
+
results.push(HashMap::from([
|
127
|
+
("x".to_string(), found.x),
|
128
|
+
("y".to_string(), found.y),
|
129
|
+
]));
|
130
|
+
}
|
131
|
+
results
|
132
|
+
}
|
133
|
+
|
134
|
+
pub fn all(&self, image_path: String, tolerance: Option<f64>) -> Vec<HashMap<String, f64>> {
|
135
|
+
let mut results = vec![];
|
136
|
+
|
137
|
+
let mut image = self.load_image("./tmp/last-screenshot.png");
|
138
|
+
let template_image = self.load_image(&image_path);
|
139
|
+
let mut matches: Vec<core::Point> = Vec::new();
|
140
|
+
|
141
|
+
// Fake out min_val for first run through loop
|
142
|
+
loop {
|
143
|
+
match self.match_template_and_replace(
|
144
|
+
&mut image,
|
145
|
+
&template_image,
|
146
|
+
tolerance.unwrap_or(0.5)
|
147
|
+
) {
|
148
|
+
Some(point) => {
|
149
|
+
matches.push(point);
|
150
|
+
}
|
151
|
+
None => break,
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
matches.iter().for_each(|point| {
|
156
|
+
results.push(HashMap::from([
|
157
|
+
("x".to_string(), point.x as f64),
|
158
|
+
("y".to_string(), point.y as f64),
|
159
|
+
]));
|
160
|
+
});
|
161
|
+
|
162
|
+
results
|
163
|
+
}
|
164
|
+
|
165
|
+
pub fn count(&self, image_path: String, tolerance: Option<f64>) -> u64 {
|
166
|
+
if let Ok(image) = open(image_path) {
|
167
|
+
let needle = autopilot::bitmap::Bitmap::new(image, None);
|
168
|
+
return self.0.count_of_bitmap(&needle, tolerance, None, None);
|
169
|
+
}
|
170
|
+
0
|
171
|
+
}
|
172
|
+
|
173
|
+
fn minimum_point(&self, mat: &Mat) -> (core::Point, f64) {
|
174
|
+
let mut min_val = 1.;
|
175
|
+
let mut min_point = core::Point { x: 0, y: 0 };
|
176
|
+
|
177
|
+
match core::min_max_loc(
|
178
|
+
&mat,
|
179
|
+
Some(&mut min_val),
|
180
|
+
None,
|
181
|
+
Some(&mut min_point),
|
182
|
+
None,
|
183
|
+
&core::no_array(),
|
184
|
+
) {
|
185
|
+
Ok(_) => {}
|
186
|
+
Err(error) => panic!("Could not find the min max loc: {}", error),
|
187
|
+
}
|
188
|
+
|
189
|
+
(min_point, min_val)
|
190
|
+
}
|
191
|
+
|
192
|
+
fn load_image(&self, path: &str) -> Mat {
|
193
|
+
match opencv::imgcodecs::imread(path, opencv::imgcodecs::IMREAD_COLOR) {
|
194
|
+
Ok(src) => src,
|
195
|
+
Err(error) => panic!("Could not read the image: {}", error),
|
196
|
+
}
|
197
|
+
}
|
198
|
+
|
199
|
+
// fn write_image(&self, path: &str, image: &Mat) {
|
200
|
+
// match opencv::imgcodecs::imwrite(path, image, &core::Vector::<i32>::new()) {
|
201
|
+
// Ok(_) => {}
|
202
|
+
// Err(error) => panic!("Could not write the image: {}", error),
|
203
|
+
// }
|
204
|
+
// }
|
205
|
+
|
206
|
+
fn match_template(&self, image: &Mat, template: &Mat, threshold: f64) -> Option<core::Point> {
|
207
|
+
let mut result = Mat::default();
|
208
|
+
|
209
|
+
imgproc::match_template_def(image, &template, &mut result, imgproc::TM_SQDIFF_NORMED)
|
210
|
+
.unwrap();
|
211
|
+
|
212
|
+
let (min_point, min_val) = self.minimum_point(&result);
|
213
|
+
|
214
|
+
if min_val > threshold {
|
215
|
+
return None;
|
216
|
+
}
|
217
|
+
|
218
|
+
Some(min_point)
|
219
|
+
}
|
220
|
+
|
221
|
+
fn match_template_and_replace(
|
222
|
+
&self,
|
223
|
+
image: &mut Mat,
|
224
|
+
template: &Mat,
|
225
|
+
threshold: f64,
|
226
|
+
) -> Option<core::Point> {
|
227
|
+
let min_point = self.match_template(image, template, threshold)?;
|
228
|
+
let template_size = template.size().unwrap();
|
229
|
+
|
230
|
+
let rect = Rect {
|
231
|
+
x: min_point.x,
|
232
|
+
y: min_point.y,
|
233
|
+
width: template_size.width as i32 + 1,
|
234
|
+
height: template_size.height as i32 + 1,
|
235
|
+
};
|
236
|
+
|
237
|
+
imgproc::rectangle(image, rect, Scalar::new(0.0, 0.0, 0.0, 0.0), -1, 8, 0).unwrap();
|
238
|
+
|
239
|
+
Some(min_point)
|
240
|
+
}
|
241
|
+
}
|
242
|
+
|
243
|
+
pub fn capture_screen_portion(x: f64, y: f64, width: f64, height: f64) -> Option<Bitmap> {
|
244
|
+
let point = autopilot::geometry::Point::new(x, y);
|
245
|
+
let size = autopilot::geometry::Size::new(width, height);
|
246
|
+
let rect = autopilot::geometry::Rect::new(point, size);
|
247
|
+
let image = autopilot::bitmap::capture_screen_portion(rect);
|
248
|
+
|
249
|
+
match image {
|
250
|
+
Ok(image) => {
|
251
|
+
image.image.save("./tmp/last-screenshot.png").unwrap();
|
252
|
+
Some(Bitmap::new(image))
|
253
|
+
}
|
254
|
+
Err(_) => None,
|
255
|
+
}
|
256
|
+
}
|
257
|
+
|
258
|
+
pub fn capture_screen() -> Option<Bitmap> {
|
259
|
+
let image = autopilot::bitmap::capture_screen();
|
260
|
+
|
261
|
+
match image {
|
262
|
+
Ok(image) => {
|
263
|
+
image.image.save("./tmp/last-screenshot.png").unwrap();
|
264
|
+
Some(Bitmap::new(image))
|
265
|
+
}
|
266
|
+
Err(_) => None,
|
267
|
+
}
|
268
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
use magnus::RArray;
|
2
|
+
extern crate autopilot;
|
3
|
+
|
4
|
+
fn key_flags(symbols: Vec<String>) -> Vec<autopilot::key::Flag> {
|
5
|
+
symbols
|
6
|
+
.iter()
|
7
|
+
.filter_map(|symbol| key_flag(symbol))
|
8
|
+
.collect::<Vec<autopilot::key::Flag>>()
|
9
|
+
}
|
10
|
+
|
11
|
+
fn key_flag(symbol: &String) -> Option<autopilot::key::Flag> {
|
12
|
+
match symbol.as_str() {
|
13
|
+
"shift" => Some(autopilot::key::Flag::Shift),
|
14
|
+
"control" => Some(autopilot::key::Flag::Control),
|
15
|
+
"alt" => Some(autopilot::key::Flag::Alt),
|
16
|
+
"meta" => Some(autopilot::key::Flag::Meta),
|
17
|
+
"help" => Some(autopilot::key::Flag::Help),
|
18
|
+
_ => None,
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
pub fn type_string(string: String, _flag: RArray, wpm: f64, noise: f64) -> () {
|
23
|
+
let _flags = _flag.to_vec::<String>().unwrap();
|
24
|
+
let flags = key_flags(_flags);
|
25
|
+
autopilot::key::type_string(&string, &flags, wpm, noise);
|
26
|
+
}
|
27
|
+
|
28
|
+
pub fn toggle_key(_key: char, down: bool, _flags: RArray, modifier_delay_ms: u64) -> () {
|
29
|
+
let flags = key_flags(_flags.to_vec::<String>().unwrap());
|
30
|
+
let key = autopilot::key::Character(_key);
|
31
|
+
autopilot::key::toggle(&key, down, &flags, modifier_delay_ms);
|
32
|
+
}
|
33
|
+
|
34
|
+
pub fn tap_key(_key: char, _flags: RArray, delay_ms: u64, modifier_delay_ms: u64) -> () {
|
35
|
+
let flags = key_flags(_flags.to_vec::<String>().unwrap());
|
36
|
+
let key = autopilot::key::Character(_key);
|
37
|
+
autopilot::key::tap(&key, &flags, delay_ms, modifier_delay_ms);
|
38
|
+
}
|
@@ -0,0 +1,49 @@
|
|
1
|
+
use magnus::{class, function, method, prelude::*, Error, Ruby};
|
2
|
+
extern crate autopilot;
|
3
|
+
|
4
|
+
mod bitmap;
|
5
|
+
mod keys;
|
6
|
+
mod mouse;
|
7
|
+
mod screen;
|
8
|
+
mod alert;
|
9
|
+
|
10
|
+
#[magnus::init]
|
11
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
12
|
+
let main_module = ruby.define_module("Deskbot")?.define_module("Providers")?;
|
13
|
+
let module = main_module.define_module("Autopilot")?;
|
14
|
+
module.define_singleton_method("type", function!(keys::type_string, 4))?;
|
15
|
+
module.define_singleton_method("toggle_key", function!(keys::toggle_key, 4))?;
|
16
|
+
module.define_singleton_method("tap_key", function!(keys::tap_key, 4))?;
|
17
|
+
|
18
|
+
module.define_singleton_method("mouse_location", function!(mouse::location, 0))?;
|
19
|
+
module.define_singleton_method("move_mouse", function!(mouse::move_to, 2))?;
|
20
|
+
module.define_singleton_method("toggle_mouse", function!(mouse::toggle, 2))?;
|
21
|
+
module.define_singleton_method("click", function!(mouse::click, 2))?;
|
22
|
+
module.define_singleton_method("scroll", function!(mouse::scroll, 2))?;
|
23
|
+
module.define_singleton_method("smooth_move_mouse", function!(mouse::smooth_move, 3))?;
|
24
|
+
|
25
|
+
module.define_singleton_method("color_at", function!(screen::get_color, 2))?;
|
26
|
+
module.define_singleton_method("screen_size", function!(screen::size, 0))?;
|
27
|
+
module.define_singleton_method("screen_scale", function!(screen::scale, 0))?;
|
28
|
+
module.define_singleton_method("is_point_visible", function!(screen::is_point_visible, 2))?;
|
29
|
+
module.define_singleton_method("is_area_visible", function!(screen::is_rect_visible, 4))?;
|
30
|
+
|
31
|
+
module.define_singleton_method("alert", function!(alert::alert, 1))?;
|
32
|
+
|
33
|
+
module.define_singleton_method(
|
34
|
+
"capture_screen_area",
|
35
|
+
function!(bitmap::capture_screen_portion, 4),
|
36
|
+
)?;
|
37
|
+
module.define_singleton_method("capture_screen", function!(bitmap::capture_screen, 0))?;
|
38
|
+
|
39
|
+
let bitmap = module.define_class("Bitmap", class::object())?;
|
40
|
+
bitmap.define_method("bounds", method!(bitmap::Bitmap::bounds, 0))?;
|
41
|
+
bitmap.define_method("find", method!(bitmap::Bitmap::find, 2))?;
|
42
|
+
bitmap.define_method("all", method!(bitmap::Bitmap::all, 2))?;
|
43
|
+
bitmap.define_method("count", method!(bitmap::Bitmap::count, 2))?;
|
44
|
+
bitmap.define_method("find_color", method!(bitmap::Bitmap::find_color, 2))?;
|
45
|
+
bitmap.define_method("all_color", method!(bitmap::Bitmap::all_color, 2))?;
|
46
|
+
bitmap.define_method("color_at", method!(bitmap::Bitmap::get_pixel, 2))?;
|
47
|
+
bitmap.define_method("save", method!(bitmap::Bitmap::save, 1))?;
|
48
|
+
Ok(())
|
49
|
+
}
|