deskbot 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|