Skip to content

Feature/259/get screen pixel color #325

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Nov 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added e2e/assets/checkers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from "./lib/provider";
export {jestMatchers} from "./lib/expect/jest.matcher.function";
export {sleep} from "./lib/sleep.function";
export {Image} from "./lib/image.class";
export {RGBA} from "./lib/rgba.class";
export {Key} from "./lib/key.enum";
export {Button} from "./lib/button.enum";
export {centerOf, randomPointIn} from "./lib/location.function";
Expand Down
7 changes: 7 additions & 0 deletions lib/provider/image-processor.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {Point} from "../point.class";
import {RGBA} from "../rgba.class";
import {Image} from "../image.class";

export interface ImageProcessor {
colorAt(image: Image | Promise<Image>, location: Point | Promise<Point>): Promise<RGBA>;
}
24 changes: 24 additions & 0 deletions lib/provider/image/jimp-image-processor.class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Jimp from 'jimp';
import {Image} from "../../image.class";
import {Point} from "../../point.class";
import {ImageProcessor} from "../image-processor.interface";
import {imageToJimp} from "../io/imageToJimp.function";
import {RGBA} from "../../rgba.class";

export default class implements ImageProcessor {
colorAt(image: Image | Promise<Image>, point: Point | Promise<Point>): Promise<RGBA> {
return new Promise<RGBA>(async (resolve, reject) => {
const location = await point;
const img = await image;
if (location.x < 0 || location.x >= img.width) {
reject(`Query location out of bounds. Should be in range 0 <= x < image.width, is ${location.x}`);
}
if (location.y < 0 || location.y >= img.height) {
reject(`Query location out of bounds. Should be in range 0 <= y < image.height, is ${location.y}`);
}
const jimpImage = imageToJimp(img);
const rgba = Jimp.intToRGBA(jimpImage.getPixelColor(location.x, location.y));
resolve(new RGBA(rgba.r, rgba.g, rgba.b, rgba.a));
});
}
}
38 changes: 38 additions & 0 deletions lib/provider/io/imageToJimp.function.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {Image} from "../../image.class";
import {imageToJimp} from "./imageToJimp.function";
import Jimp from "jimp";

jest.mock('jimp', () => {
class JimpMock {
bitmap = {
width: 100,
height: 100,
data: Buffer.from([]),
}
hasAlpha = () => false
static read = jest.fn(() => Promise.resolve(new JimpMock()))
}

return ({
__esModule: true,
default: JimpMock
})
});

afterEach(() => jest.resetAllMocks());

describe('imageToJimp', () => {
it('should successfully convert an Image to a Jimp instance', async () => {
// GIVEN
const scanMock = jest.fn();
Jimp.prototype.scan = scanMock;
const inputImage = new Image(1, 1, Buffer.from([0, 0, 0]),3, "input_image");

// WHEN
const result = await imageToJimp(inputImage);

// THEN
expect(result).toBeInstanceOf(Jimp);
expect(scanMock).toHaveBeenCalledTimes(1);
});
});
17 changes: 17 additions & 0 deletions lib/provider/io/imageToJimp.function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Jimp from "jimp";
import {Image} from "../../image.class";

export function imageToJimp(image: Image): Jimp {
const jimpImage = new Jimp({
data: image.data,
width: image.width,
height: image.height
});
// Images treat data in BGR format, so we have to switch red and blue color channels
jimpImage.scan(0, 0, jimpImage.bitmap.width, jimpImage.bitmap.height, function (_, __, idx) {
const red = this.bitmap.data[idx];
this.bitmap.data[idx] = this.bitmap.data[idx + 2];
this.bitmap.data[idx + 2] = red;
});
return jimpImage;
}
1 change: 0 additions & 1 deletion lib/provider/io/jimp-image-reader.class.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import ImageReader from "./jimp-image-reader.class";
import {join} from "path";
import Jimp from "jimp";

jest.mock('gifwrap', () => {});
jest.mock('jimp', () => {
class JimpMock {
bitmap = {
Expand Down
1 change: 0 additions & 1 deletion lib/provider/io/jimp-image-writer.class.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import ImageWriter from "./jimp-image-writer.class";
import {Image} from "../../image.class";
import Jimp from "jimp";

jest.mock('gifwrap', () => {});
jest.mock('jimp', () => {
class JimpMock {
bitmap = {
Expand Down
14 changes: 2 additions & 12 deletions lib/provider/io/jimp-image-writer.class.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import Jimp from 'jimp';
import {ImageWriter, ImageWriterParameters} from "../image-writer.type";
import {imageToJimp} from "./imageToJimp.function";

export default class implements ImageWriter {
store(parameters: ImageWriterParameters): Promise<void> {
return new Promise((resolve, reject) => {
const jimpImage = new Jimp({
data: parameters.data.data,
width: parameters.data.width,
height: parameters.data.height
});
// libnut returns data in BGR format, so we have to switch red and blue color channels
jimpImage.scan(0, 0, jimpImage.bitmap.width, jimpImage.bitmap.height, function (_, __, idx) {
const red = this.bitmap.data[idx];
this.bitmap.data[idx] = this.bitmap.data[idx + 2];
this.bitmap.data[idx + 2] = red;
});
const jimpImage = imageToJimp(parameters.data);
jimpImage
.writeAsync(parameters.path)
.then(_ => resolve())
Expand Down
35 changes: 32 additions & 3 deletions lib/provider/provider-registry.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,48 @@ import Screen from "./native/libnut-screen.class";
import Window from "./native/libnut-window.class";
import {ImageReader} from "./image-reader.type";
import {ImageWriter} from "./image-writer.type";
import {ImageProcessor} from "./image-processor.interface";

import ImageReaderImpl from "./io/jimp-image-reader.class";
import ImageWriterImpl from "./io/jimp-image-writer.class";
import ImageProcessorImpl from "./image/jimp-image-processor.class";

export interface ProviderRegistry {
getClipboard(): ClipboardProviderInterface;
registerClipboardProvider(value: ClipboardProviderInterface): void;

getImageFinder(): ImageFinderInterface;
registerImageFinder(value: ImageFinderInterface): void;
registerClipboardProvider(value: ClipboardProviderInterface): void;

getKeyboard(): KeyboardProviderInterface;

registerKeyboardProvider(value: KeyboardProviderInterface): void;

getMouse(): MouseProviderInterface;

registerMouseProvider(value: MouseProviderInterface): void;

getScreen(): ScreenProviderInterface;

registerScreenProvider(value: ScreenProviderInterface): void;

getWindow(): WindowProviderInterface;

registerWindowProvider(value: WindowProviderInterface): void;

getImageFinder(): ImageFinderInterface;

registerImageFinder(value: ImageFinderInterface): void;

getImageReader(): ImageReader;

registerImageReader(value: ImageReader): void;

getImageWriter(): ImageWriter;

registerImageWriter(value: ImageWriter): void;

getImageProcessor(): ImageProcessor;

registerImageProcessor(value: ImageProcessor): void;
}

class DefaultProviderRegistry implements ProviderRegistry {
Expand All @@ -51,6 +65,7 @@ class DefaultProviderRegistry implements ProviderRegistry {
private _window?: WindowProviderInterface;
private _imageReader?: ImageReader;
private _imageWriter?: ImageWriter;
private _imageProcessor?: ImageProcessor;

getClipboard(): ClipboardProviderInterface {
if (this._clipboard) {
Expand Down Expand Up @@ -124,6 +139,7 @@ class DefaultProviderRegistry implements ProviderRegistry {
}
throw new Error(`No ImageReader registered`);
}

registerImageReader(value: ImageReader) {
this._imageReader = value;
}
Expand All @@ -134,9 +150,21 @@ class DefaultProviderRegistry implements ProviderRegistry {
}
throw new Error(`No ImageWriter registered`);
}

registerImageWriter(value: ImageWriter) {
this._imageWriter = value;
}

getImageProcessor(): ImageProcessor {
if (this._imageProcessor) {
return this._imageProcessor;
}
throw new Error(`No ImageProcessor registered`);
}

registerImageProcessor(value: ImageProcessor): void {
this._imageProcessor = value;
}
}

const providerRegistry = new DefaultProviderRegistry();
Expand All @@ -148,5 +176,6 @@ providerRegistry.registerScreenProvider(new Screen());
providerRegistry.registerWindowProvider(new Window());
providerRegistry.registerImageWriter(new ImageWriterImpl());
providerRegistry.registerImageReader(new ImageReaderImpl());
providerRegistry.registerImageProcessor(new ImageProcessorImpl());

export default providerRegistry;
13 changes: 13 additions & 0 deletions lib/rgba.class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class RGBA {
constructor(public readonly R: number, public readonly G: number, public readonly B: number, public readonly A: number) {
}

public toString(): string {
return `rgb(${this.R},${this.G},${this.B})`;
}

public toHex(): string {
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')}`
}
}

10 changes: 10 additions & 0 deletions lib/screen.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {Image} from "./image.class";
import {ProviderRegistry} from "./provider/provider-registry.class";
import {loadImageResource} from "./imageResources.function";
import {FirstArgumentType} from "./typings";
import {Point} from "./point.class";

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

Expand Down Expand Up @@ -308,6 +309,15 @@ export class ScreenClass {
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 = this.providerRegistry.getScreen().grabScreen();
return this.providerRegistry.getImageProcessor().colorAt(screenContent, point);
}

private async saveImage(
image: Image,
fileName: string,
Expand Down
44 changes: 44 additions & 0 deletions lib/screen.colorAt.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {Image, loadImage, Point, Region, RGBA, ScreenClass, ScreenProviderInterface} from "../index";
import {mockPartial} from "sneer";
import providerRegistry, {ProviderRegistry} from "./provider/provider-registry.class";
import {ImageProcessor} from "./provider/image-processor.interface";

const searchRegion = new Region(0, 0, 1000, 1000);
const providerRegistryMock = mockPartial<ProviderRegistry>({
getScreen(): ScreenProviderInterface {
return mockPartial<ScreenProviderInterface>({
grabScreenRegion(): Promise<Image> {
return Promise.resolve(new Image(searchRegion.width, searchRegion.height, new ArrayBuffer(0), 3, "needle_image"));
},
screenSize(): Promise<Region> {
return Promise.resolve(searchRegion);
}
})
},
getImageProcessor(): ImageProcessor {
return providerRegistry.getImageProcessor();
}
});

describe("colorAt", () => {
it("should return the correct RGBA value for a given pixel", async () => {
// GIVEN
const screenshot = loadImage(`${__dirname}/../e2e/assets/checkers.png`);
const grabScreenMock = jest.fn(() => Promise.resolve(screenshot));
providerRegistryMock.getScreen = jest.fn(() => mockPartial<ScreenProviderInterface>({
grabScreen: grabScreenMock
}));
providerRegistryMock.getImageProcessor()
const SUT = new ScreenClass(providerRegistryMock);
const expectedWhite = new RGBA(255, 255, 255, 255);
const expectedBlack = new RGBA(0, 0, 0, 255);

// WHEN
const white = await SUT.colorAt(new Point(64, 64));
const black = await SUT.colorAt(new Point(192, 64));

// THEN
expect(white).toStrictEqual(expectedWhite);
expect(black).toStrictEqual(expectedBlack);
});
});