Skip to content

Commit 719f27e

Browse files
authored
Feature/259/get screen pixel color (#325)
* (#259) Introduced ImageProcessor interface * (#259) imageToJimp converter function * (#259) RGBA dataclass * (#259) Implement ImageProcessor and register the new provider * (#259) Re-use imageToJimp and clean up tests * (#259) Fixed import for Image which was previously importing from dist * Maintenance/310/remove adapters (#311) * (#310) Delete both adapters * (#310) Migrate window.function to not use adapters * (#310) Migrated window.class to not use adapters * (#310) Migrated screen.class to not use adapters * (#310) Migrated movement.function to not use adapters * (#310) Migrated mouse.class to not use adapters * (#310) Remove leftover notions of adapters in test description * (#310) Fixed expected format when saving a screenshot to disk * (#310) Migrated keyboard.class to not use adapters * (#310) Migrated clipboard.class to not use adapters * (#310) Migrated assert.class test to not use adapters * (#310) Migrated all exported instances to not use adapters * (#310) Removed mention of adapter from test description * Added sponsors listing to README.md * (#306) Update supported cpus to include Apple Silicon (arm64) * Add https://github.com/stoefln to sponsors listing * Feature/204/screen find image needles (#319) * (#204) Enabled passing of Image data to Screen#find * (#204) Updated screen tests * (#204) Removed plugin test from main repo * (#204) Removed plugin test from Docker test runs * (#204) Introduced additional id property to images * (#204) Adjusted tests and image usage to new id property * (#204) Added helper function to make loading image resources easier * (#204) Adapted toShow matcher to new find parameters * (#204) Re-use loadImageResource function * Feature/320/find accept promise (#322) * (#320) Make find, and the functions re-using it, accept Promise<Image> * (#320) Update toShow matcher accordingly * (#320) Update tests * (#321) Added `findAll` to Screen (#323) * (#259) Add `colorAt` to screen class
1 parent 3498963 commit 719f27e

13 files changed

+188
-17
lines changed

e2e/assets/checkers.png

1.58 KB
Loading

index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export * from "./lib/provider";
2525
export {jestMatchers} from "./lib/expect/jest.matcher.function";
2626
export {sleep} from "./lib/sleep.function";
2727
export {Image} from "./lib/image.class";
28+
export {RGBA} from "./lib/rgba.class";
2829
export {Key} from "./lib/key.enum";
2930
export {Button} from "./lib/button.enum";
3031
export {centerOf, randomPointIn} from "./lib/location.function";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {Point} from "../point.class";
2+
import {RGBA} from "../rgba.class";
3+
import {Image} from "../image.class";
4+
5+
export interface ImageProcessor {
6+
colorAt(image: Image | Promise<Image>, location: Point | Promise<Point>): Promise<RGBA>;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Jimp from 'jimp';
2+
import {Image} from "../../image.class";
3+
import {Point} from "../../point.class";
4+
import {ImageProcessor} from "../image-processor.interface";
5+
import {imageToJimp} from "../io/imageToJimp.function";
6+
import {RGBA} from "../../rgba.class";
7+
8+
export default class implements ImageProcessor {
9+
colorAt(image: Image | Promise<Image>, point: Point | Promise<Point>): Promise<RGBA> {
10+
return new Promise<RGBA>(async (resolve, reject) => {
11+
const location = await point;
12+
const img = await image;
13+
if (location.x < 0 || location.x >= img.width) {
14+
reject(`Query location out of bounds. Should be in range 0 <= x < image.width, is ${location.x}`);
15+
}
16+
if (location.y < 0 || location.y >= img.height) {
17+
reject(`Query location out of bounds. Should be in range 0 <= y < image.height, is ${location.y}`);
18+
}
19+
const jimpImage = imageToJimp(img);
20+
const rgba = Jimp.intToRGBA(jimpImage.getPixelColor(location.x, location.y));
21+
resolve(new RGBA(rgba.r, rgba.g, rgba.b, rgba.a));
22+
});
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {Image} from "../../image.class";
2+
import {imageToJimp} from "./imageToJimp.function";
3+
import Jimp from "jimp";
4+
5+
jest.mock('jimp', () => {
6+
class JimpMock {
7+
bitmap = {
8+
width: 100,
9+
height: 100,
10+
data: Buffer.from([]),
11+
}
12+
hasAlpha = () => false
13+
static read = jest.fn(() => Promise.resolve(new JimpMock()))
14+
}
15+
16+
return ({
17+
__esModule: true,
18+
default: JimpMock
19+
})
20+
});
21+
22+
afterEach(() => jest.resetAllMocks());
23+
24+
describe('imageToJimp', () => {
25+
it('should successfully convert an Image to a Jimp instance', async () => {
26+
// GIVEN
27+
const scanMock = jest.fn();
28+
Jimp.prototype.scan = scanMock;
29+
const inputImage = new Image(1, 1, Buffer.from([0, 0, 0]),3, "input_image");
30+
31+
// WHEN
32+
const result = await imageToJimp(inputImage);
33+
34+
// THEN
35+
expect(result).toBeInstanceOf(Jimp);
36+
expect(scanMock).toHaveBeenCalledTimes(1);
37+
});
38+
});
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Jimp from "jimp";
2+
import {Image} from "../../image.class";
3+
4+
export function imageToJimp(image: Image): Jimp {
5+
const jimpImage = new Jimp({
6+
data: image.data,
7+
width: image.width,
8+
height: image.height
9+
});
10+
// Images treat data in BGR format, so we have to switch red and blue color channels
11+
jimpImage.scan(0, 0, jimpImage.bitmap.width, jimpImage.bitmap.height, function (_, __, idx) {
12+
const red = this.bitmap.data[idx];
13+
this.bitmap.data[idx] = this.bitmap.data[idx + 2];
14+
this.bitmap.data[idx + 2] = red;
15+
});
16+
return jimpImage;
17+
}

lib/provider/io/jimp-image-reader.class.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import ImageReader from "./jimp-image-reader.class";
22
import {join} from "path";
33
import Jimp from "jimp";
44

5-
jest.mock('gifwrap', () => {});
65
jest.mock('jimp', () => {
76
class JimpMock {
87
bitmap = {

lib/provider/io/jimp-image-writer.class.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import ImageWriter from "./jimp-image-writer.class";
22
import {Image} from "../../image.class";
33
import Jimp from "jimp";
44

5-
jest.mock('gifwrap', () => {});
65
jest.mock('jimp', () => {
76
class JimpMock {
87
bitmap = {

lib/provider/io/jimp-image-writer.class.ts

+2-12
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,10 @@
1-
import Jimp from 'jimp';
21
import {ImageWriter, ImageWriterParameters} from "../image-writer.type";
2+
import {imageToJimp} from "./imageToJimp.function";
33

44
export default class implements ImageWriter {
55
store(parameters: ImageWriterParameters): Promise<void> {
66
return new Promise((resolve, reject) => {
7-
const jimpImage = new Jimp({
8-
data: parameters.data.data,
9-
width: parameters.data.width,
10-
height: parameters.data.height
11-
});
12-
// libnut returns data in BGR format, so we have to switch red and blue color channels
13-
jimpImage.scan(0, 0, jimpImage.bitmap.width, jimpImage.bitmap.height, function (_, __, idx) {
14-
const red = this.bitmap.data[idx];
15-
this.bitmap.data[idx] = this.bitmap.data[idx + 2];
16-
this.bitmap.data[idx + 2] = red;
17-
});
7+
const jimpImage = imageToJimp(parameters.data);
188
jimpImage
199
.writeAsync(parameters.path)
2010
.then(_ => resolve())

lib/provider/provider-registry.class.ts

+32-3
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,48 @@ import Screen from "./native/libnut-screen.class";
1212
import Window from "./native/libnut-window.class";
1313
import {ImageReader} from "./image-reader.type";
1414
import {ImageWriter} from "./image-writer.type";
15+
import {ImageProcessor} from "./image-processor.interface";
1516

1617
import ImageReaderImpl from "./io/jimp-image-reader.class";
1718
import ImageWriterImpl from "./io/jimp-image-writer.class";
19+
import ImageProcessorImpl from "./image/jimp-image-processor.class";
1820

1921
export interface ProviderRegistry {
2022
getClipboard(): ClipboardProviderInterface;
21-
registerClipboardProvider(value: ClipboardProviderInterface): void;
2223

23-
getImageFinder(): ImageFinderInterface;
24-
registerImageFinder(value: ImageFinderInterface): void;
24+
registerClipboardProvider(value: ClipboardProviderInterface): void;
2525

2626
getKeyboard(): KeyboardProviderInterface;
27+
2728
registerKeyboardProvider(value: KeyboardProviderInterface): void;
2829

2930
getMouse(): MouseProviderInterface;
31+
3032
registerMouseProvider(value: MouseProviderInterface): void;
3133

3234
getScreen(): ScreenProviderInterface;
35+
3336
registerScreenProvider(value: ScreenProviderInterface): void;
3437

3538
getWindow(): WindowProviderInterface;
39+
3640
registerWindowProvider(value: WindowProviderInterface): void;
3741

42+
getImageFinder(): ImageFinderInterface;
43+
44+
registerImageFinder(value: ImageFinderInterface): void;
45+
3846
getImageReader(): ImageReader;
47+
3948
registerImageReader(value: ImageReader): void;
4049

4150
getImageWriter(): ImageWriter;
51+
4252
registerImageWriter(value: ImageWriter): void;
53+
54+
getImageProcessor(): ImageProcessor;
55+
56+
registerImageProcessor(value: ImageProcessor): void;
4357
}
4458

4559
class DefaultProviderRegistry implements ProviderRegistry {
@@ -51,6 +65,7 @@ class DefaultProviderRegistry implements ProviderRegistry {
5165
private _window?: WindowProviderInterface;
5266
private _imageReader?: ImageReader;
5367
private _imageWriter?: ImageWriter;
68+
private _imageProcessor?: ImageProcessor;
5469

5570
getClipboard(): ClipboardProviderInterface {
5671
if (this._clipboard) {
@@ -124,6 +139,7 @@ class DefaultProviderRegistry implements ProviderRegistry {
124139
}
125140
throw new Error(`No ImageReader registered`);
126141
}
142+
127143
registerImageReader(value: ImageReader) {
128144
this._imageReader = value;
129145
}
@@ -134,9 +150,21 @@ class DefaultProviderRegistry implements ProviderRegistry {
134150
}
135151
throw new Error(`No ImageWriter registered`);
136152
}
153+
137154
registerImageWriter(value: ImageWriter) {
138155
this._imageWriter = value;
139156
}
157+
158+
getImageProcessor(): ImageProcessor {
159+
if (this._imageProcessor) {
160+
return this._imageProcessor;
161+
}
162+
throw new Error(`No ImageProcessor registered`);
163+
}
164+
165+
registerImageProcessor(value: ImageProcessor): void {
166+
this._imageProcessor = value;
167+
}
140168
}
141169

142170
const providerRegistry = new DefaultProviderRegistry();
@@ -148,5 +176,6 @@ providerRegistry.registerScreenProvider(new Screen());
148176
providerRegistry.registerWindowProvider(new Window());
149177
providerRegistry.registerImageWriter(new ImageWriterImpl());
150178
providerRegistry.registerImageReader(new ImageReaderImpl());
179+
providerRegistry.registerImageProcessor(new ImageProcessorImpl());
151180

152181
export default providerRegistry;

lib/rgba.class.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class RGBA {
2+
constructor(public readonly R: number, public readonly G: number, public readonly B: number, public readonly A: number) {
3+
}
4+
5+
public toString(): string {
6+
return `rgb(${this.R},${this.G},${this.B})`;
7+
}
8+
9+
public toHex(): string {
10+
return `#${this.R.toString(16).padStart(2, '0')}${this.G.toString(16).padStart(2, '0')}${this.B.toString(16).padStart(2, '0')}${this.A.toString(16).padStart(2, '0')}`
11+
}
12+
}
13+

lib/screen.class.ts

+10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {Image} from "./image.class";
1010
import {ProviderRegistry} from "./provider/provider-registry.class";
1111
import {loadImageResource} from "./imageResources.function";
1212
import {FirstArgumentType} from "./typings";
13+
import {Point} from "./point.class";
1314

1415
export type FindHookCallback = (target: MatchResult) => Promise<void>;
1516

@@ -308,6 +309,15 @@ export class ScreenClass {
308309
return this.providerRegistry.getScreen().grabScreenRegion(await regionToGrab);
309310
}
310311

312+
/**
313+
* {@link colorAt} returns RGBA color values for a certain pixel at {@link Point} p
314+
* @param point Location to query color information from
315+
*/
316+
public async colorAt(point: Point | Promise<Point>) {
317+
const screenContent = this.providerRegistry.getScreen().grabScreen();
318+
return this.providerRegistry.getImageProcessor().colorAt(screenContent, point);
319+
}
320+
311321
private async saveImage(
312322
image: Image,
313323
fileName: string,

lib/screen.colorAt.spec.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {Image, loadImage, Point, Region, RGBA, ScreenClass, ScreenProviderInterface} from "../index";
2+
import {mockPartial} from "sneer";
3+
import providerRegistry, {ProviderRegistry} from "./provider/provider-registry.class";
4+
import {ImageProcessor} from "./provider/image-processor.interface";
5+
6+
const searchRegion = new Region(0, 0, 1000, 1000);
7+
const providerRegistryMock = mockPartial<ProviderRegistry>({
8+
getScreen(): ScreenProviderInterface {
9+
return mockPartial<ScreenProviderInterface>({
10+
grabScreenRegion(): Promise<Image> {
11+
return Promise.resolve(new Image(searchRegion.width, searchRegion.height, new ArrayBuffer(0), 3, "needle_image"));
12+
},
13+
screenSize(): Promise<Region> {
14+
return Promise.resolve(searchRegion);
15+
}
16+
})
17+
},
18+
getImageProcessor(): ImageProcessor {
19+
return providerRegistry.getImageProcessor();
20+
}
21+
});
22+
23+
describe("colorAt", () => {
24+
it("should return the correct RGBA value for a given pixel", async () => {
25+
// GIVEN
26+
const screenshot = loadImage(`${__dirname}/../e2e/assets/checkers.png`);
27+
const grabScreenMock = jest.fn(() => Promise.resolve(screenshot));
28+
providerRegistryMock.getScreen = jest.fn(() => mockPartial<ScreenProviderInterface>({
29+
grabScreen: grabScreenMock
30+
}));
31+
providerRegistryMock.getImageProcessor()
32+
const SUT = new ScreenClass(providerRegistryMock);
33+
const expectedWhite = new RGBA(255, 255, 255, 255);
34+
const expectedBlack = new RGBA(0, 0, 0, 255);
35+
36+
// WHEN
37+
const white = await SUT.colorAt(new Point(64, 64));
38+
const black = await SUT.colorAt(new Point(192, 64));
39+
40+
// THEN
41+
expect(white).toStrictEqual(expectedWhite);
42+
expect(black).toStrictEqual(expectedBlack);
43+
});
44+
});

0 commit comments

Comments
 (0)