-
-
Notifications
You must be signed in to change notification settings - Fork 150
/
Copy pathscreen.class.ts
328 lines (299 loc) · 13.3 KB
/
screen.class.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
import {cwd} from "process";
import {FileType} from "./file-type.enum";
import {generateOutputPath} from "./generate-output-path.function";
import {LocationParameters} from "./locationparameters.class";
import {MatchRequest} from "./match-request.class";
import {MatchResult} from "./match-result.class";
import {Region} from "./region.class";
import {timeout} from "./util/timeout.function";
import {Image} from "./image.class";
import {ProviderRegistry} from "./provider/provider-registry.class";
import {FirstArgumentType} from "./typings";
import {Point} from "./point.class";
export type FindHookCallback = (target: MatchResult) => Promise<void>;
function validateSearchRegion(search: Region, screen: Region) {
if (search.left < 0 || search.top < 0 || search.width < 0 || search.height < 0) {
throw new Error(`Negative values in search region ${search}`)
}
if (isNaN(search.left) || isNaN(search.top) || isNaN(search.width) || isNaN(search.height)) {
throw new Error(`NaN values in search region ${search}`)
}
if (search.width < 2 || search.height < 2) {
throw new Error(`Search region ${search} is not large enough. Must be at least two pixels in both width and height.`)
}
if (search.left + search.width > screen.width || search.top + search.height > screen.height) {
throw new Error(`Search region ${search} extends beyond screen boundaries (${screen.width}x${screen.height})`)
}
}
/**
* {@link ScreenClass} class provides methods to access screen content of a systems main display
*/
export class ScreenClass {
/**
* Config object for {@link ScreenClass} class
*/
public config = {
/**
* Configures the required matching percentage for template images to be declared as a match
*/
confidence: 0.99,
/**
* Configure whether to auto highlight all search results or not
*/
autoHighlight: false,
/**
* Configure highlighting duration
*/
highlightDurationMs: 500,
/**
* Configure opacity of highlight window
*/
highlightOpacity: 0.25,
/**
* Configures the path from which template images are loaded from
*/
resourceDirectory: cwd(),
};
/**
* {@link ScreenClass} class constructor
* @param providerRegistry A {@link ProviderRegistry} used to access underlying implementations
* @param findHooks A {@link Map} of {@link FindHookCallback} methods assigned to a template image
*/
constructor(
private providerRegistry: ProviderRegistry,
private findHooks: Map<string | Image, FindHookCallback[]> = new Map<string | Image, FindHookCallback[]>()) {
}
/**
* {@link width} returns the main screen width
* This refers to the hardware resolution.
* Screens with higher pixel density (e.g. retina displays in MacBooks) might have a higher width in in actual pixels
*/
public width() {
return this.providerRegistry.getScreen().screenWidth();
}
/**
* {@link height} returns the main screen height
* This refers to the hardware resolution.
* Screens with higher pixel density (e.g. retina displays in MacBooks) might have a higher height in in actual pixels
*/
public height() {
return this.providerRegistry.getScreen().screenHeight();
}
/**
* {@link find} will search for a single occurrence of a template image on a systems main screen
* @param template Template {@link Image} instance
* @param params {@link LocationParameters} which are used to fine tune search region and / or match confidence
*/
public async find(
template: Image | Promise<Image>,
params?: LocationParameters,
): Promise<Region> {
const minMatch = (params && params.confidence) || this.config.confidence;
const screenSize = await this.providerRegistry.getScreen().screenSize();
const searchRegion = (params && params.searchRegion) || screenSize;
const searchMultipleScales = (params && params.searchMultipleScales)
const needle = await template;
const screenImage = await this.providerRegistry.getScreen().grabScreenRegion(searchRegion);
const matchRequest = new MatchRequest(
screenImage,
needle,
minMatch,
searchMultipleScales
);
return new Promise<Region>(async (resolve, reject) => {
try {
validateSearchRegion(searchRegion, screenSize);
const matchResult = await this.providerRegistry.getImageFinder().findMatch(matchRequest);
const possibleHooks = this.findHooks.get(needle) || [];
for (const hook of possibleHooks) {
await hook(matchResult);
}
const resultRegion = new Region(
searchRegion.left + matchResult.location.left,
searchRegion.top + matchResult.location.top,
matchResult.location.width,
matchResult.location.height
)
if (this.config.autoHighlight) {
resolve(this.highlight(resultRegion));
} else {
resolve(resultRegion);
}
} catch (e) {
reject(
`Searching for ${needle.id} failed. Reason: '${e}'`,
);
}
});
}
/**
* {@link findAll} will search for every occurrences of a template image on a systems main screen
* @param template Template {@link Image} instance
* @param params {@link LocationParameters} which are used to fine tune search region and / or match confidence
*/
public async findAll(
template: FirstArgumentType<typeof ScreenClass.prototype.find>,
params?: LocationParameters,
): Promise<Region[]> {
const minMatch = (params && params.confidence) || this.config.confidence;
const screenSize = await this.providerRegistry.getScreen().screenSize();
const searchRegion = (params && params.searchRegion) || screenSize;
const searchMultipleScales = (params && params.searchMultipleScales)
const needle = await template;
const screenImage = await this.providerRegistry.getScreen().grabScreenRegion(searchRegion);
const matchRequest = new MatchRequest(
screenImage,
needle,
minMatch,
searchMultipleScales
);
return new Promise<Region[]>(async (resolve, reject) => {
try {
validateSearchRegion(searchRegion, screenSize);
const matchResults = await this.providerRegistry.getImageFinder().findMatches(matchRequest);
const possibleHooks = this.findHooks.get(needle) || [];
for (const hook of possibleHooks) {
for (const matchResult of matchResults) {
await hook(matchResult);
}
}
const resultRegions = matchResults.map(matchResult => {
return new Region(
searchRegion.left + matchResult.location.left,
searchRegion.top + matchResult.location.top,
matchResult.location.width,
matchResult.location.height
)
})
if (this.config.autoHighlight) {
resultRegions.forEach(region => this.highlight(region));
resolve(resultRegions);
} else {
resolve(resultRegions);
}
} catch (e) {
reject(
`Searching for ${needle.id} failed. Reason: '${e}'`,
);
}
});
}
/**
* {@link highlight} highlights a screen {@link Region} for a certain duration by overlaying it with an opaque highlight window
* @param regionToHighlight The {@link Region} to highlight
*/
public async highlight(regionToHighlight: Region | Promise<Region>): Promise<Region> {
const highlightRegion = await regionToHighlight;
await this.providerRegistry.getScreen().highlightScreenRegion(highlightRegion, this.config.highlightDurationMs, this.config.highlightOpacity);
return highlightRegion;
}
/**
* {@link waitFor} searches for a template image for a specified duration
* @param templateImage Filename of the template image, relative to {@link ScreenClass.config.resourceDirectory}, or an {@link Image}
* @param timeoutMs Timeout in milliseconds after which {@link waitFor} fails
* @param params {@link LocationParameters} which are used to fine tune search region and / or match confidence
*/
public async waitFor(
templateImage: FirstArgumentType<typeof ScreenClass.prototype.find>,
timeoutMs: number = 5000,
params?: LocationParameters,
): Promise<Region> {
return timeout(500, timeoutMs, () => this.find(templateImage, params), {signal: params?.abort});
}
/**
* {@link on} registers a callback which is triggered once a certain template image is found
* @param templateImage Template image to trigger the callback on
* @param callback The {@link FindHookCallback} function to trigger
*/
public on(templateImage: string | Image, callback: FindHookCallback) {
const existingHooks = this.findHooks.get(templateImage) || [];
this.findHooks.set(templateImage, [...existingHooks, callback]);
}
/**
* {@link capture} captures a screenshot of a systems main display
* @param fileName Basename for the generated screenshot
* @param fileFormat The {@link FileType} for the generated screenshot
* @param filePath The output path for the generated screenshot (Default: {@link cwd})
* @param fileNamePrefix Filename prefix for the generated screenshot (Default: empty)
* @param fileNamePostfix Filename postfix for the generated screenshot (Default: empty)
*/
public async capture(
fileName: string,
fileFormat: FileType = FileType.PNG,
filePath: string = cwd(),
fileNamePrefix: string = "",
fileNamePostfix: string = ""): Promise<string> {
const currentScreen = await this.providerRegistry.getScreen().grabScreen();
return this.saveImage(
currentScreen,
fileName,
fileFormat,
filePath,
fileNamePrefix,
fileNamePostfix);
}
/**
* {@link grab} grabs screen content of a systems main display
*/
public async grab(): Promise<Image> {
return this.providerRegistry.getScreen().grabScreen();
}
/**
* {@link captureRegion} captures a screenshot of a region on the systems main display
* @param fileName Basename for the generated screenshot
* @param regionToCapture The region of the screen to capture in the screenshot
* @param fileFormat The {@link FileType} for the generated screenshot
* @param filePath The output path for the generated screenshot (Default: {@link cwd})
* @param fileNamePrefix Filename prefix for the generated screenshot (Default: empty)
* @param fileNamePostfix Filename postfix for the generated screenshot (Default: empty)
*/
public async captureRegion(
fileName: string,
regionToCapture: Region | Promise<Region>,
fileFormat: FileType = FileType.PNG,
filePath: string = cwd(),
fileNamePrefix: string = "",
fileNamePostfix: string = ""): Promise<string> {
const regionImage = await this.providerRegistry.getScreen().grabScreenRegion(await regionToCapture);
return this.saveImage(
regionImage,
fileName,
fileFormat,
filePath,
fileNamePrefix,
fileNamePostfix);
}
/**
* {@link grabRegion} grabs screen content of a region on the systems main display
* @param regionToGrab The screen region to grab
*/
public async grabRegion(regionToGrab: Region | Promise<Region>): Promise<Image> {
return this.providerRegistry.getScreen().grabScreenRegion(await regionToGrab);
}
/**
* {@link colorAt} returns RGBA color values for a certain pixel at {@link Point} p
* @param point Location to query color information from
*/
public async colorAt(point: Point | Promise<Point>) {
const screenContent = await this.providerRegistry.getScreen().grabScreen();
const inputPoint = await point;
const scaledPoint = new Point(inputPoint.x * screenContent.pixelDensity.scaleX, inputPoint.y * screenContent.pixelDensity.scaleY);
return this.providerRegistry.getImageProcessor().colorAt(screenContent, scaledPoint);
}
private async saveImage(
image: Image,
fileName: string,
fileFormat: FileType,
filePath: string,
fileNamePrefix: string,
fileNamePostfix: string) {
const outputPath = generateOutputPath(fileName, {
path: filePath,
postfix: fileNamePostfix,
prefix: fileNamePrefix,
type: fileFormat,
});
await this.providerRegistry.getImageWriter().store({data: image, path: outputPath})
return outputPath;
}
}