Skip to content

Commit 1ffaef6

Browse files
authored
Feature/336/colormode conversion (#337)
* (#336) Introduced colormode enum * (#336) Extended Image class with color mode conversion methods * (#336) Set color mode on images when reading from disk * (#336) Set colormode on images when grabbing screen content * (#336) Limit switching of channels only to BGR images * (#336) Export ColorMode enum * (#336) Chasing down another async error in tests due to jimp
1 parent ba5b810 commit 1ffaef6

File tree

9 files changed

+130
-37
lines changed

9 files changed

+130
-37
lines changed

index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export {Point} from "./lib/point.class";
3636
export {Region} from "./lib/region.class";
3737
export {Window} from "./lib/window.class";
3838
export {FileType} from "./lib/file-type.enum";
39+
export {ColorMode} from "./lib/colormode.enum";
3940

4041
const lineHelper = new LineHelper();
4142

lib/colormode.enum.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* The {@link ColorMode} enum is used to specify the color mode of an {@link Image}
3+
*/
4+
export enum ColorMode {
5+
BGR,
6+
RGB
7+
}

lib/image.class.spec.ts

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,68 @@
1-
import { Image } from "./image.class";
1+
import {Image} from "./image.class";
2+
import {imageToJimp} from "./provider/io/imageToJimp.function";
3+
import {ColorMode} from "./colormode.enum";
4+
5+
jest.mock("./provider/io/imageToJimp.function", () => {
6+
return {
7+
imageToJimp: jest.fn()
8+
}
9+
});
10+
11+
afterEach(() => {
12+
jest.resetAllMocks();
13+
});
214

315
describe("Image class", () => {
4-
it("should return alphachannel = true for > 3 channels", () => {
5-
const SUT = new Image(200, 200, 123, 4, "id");
6-
expect(SUT.hasAlphaChannel).toBeTruthy();
7-
});
8-
9-
it("should return alphachannel = false for <= 3 channels", () => {
10-
const SUT = new Image(200, 200, 123, 3, "id");
11-
expect(SUT.hasAlphaChannel).toBeFalsy();
12-
});
13-
it("should return alphachannel = false for <= 3 channels", () => {
14-
const SUT = new Image(200, 200, 123, 2, "id");
15-
expect(SUT.hasAlphaChannel).toBeFalsy();
16-
});
17-
it("should return alphachannel = false for <= 3 channels", () => {
18-
const SUT = new Image(200, 200, 123, 1, "id");
19-
expect(SUT.hasAlphaChannel).toBeFalsy();
20-
});
21-
22-
it("should throw for <= 0 channels", () => {
23-
expect(() => new Image(200, 200, 123, 0, "id")).toThrowError("Channel <= 0");
24-
});
25-
26-
it("should have a default pixel density of 1.0", () => {
27-
const SUT = new Image(200, 200, 123, 1, "id");
28-
expect(SUT.pixelDensity).toEqual({ scaleX: 1.0, scaleY: 1.0 });
29-
});
16+
it("should return alphachannel = true for > 3 channels", () => {
17+
const SUT = new Image(200, 200, 123, 4, "id");
18+
expect(SUT.hasAlphaChannel).toBeTruthy();
19+
});
20+
21+
it("should return alphachannel = false for <= 3 channels", () => {
22+
const SUT = new Image(200, 200, 123, 3, "id");
23+
expect(SUT.hasAlphaChannel).toBeFalsy();
24+
});
25+
it("should return alphachannel = false for <= 3 channels", () => {
26+
const SUT = new Image(200, 200, 123, 2, "id");
27+
expect(SUT.hasAlphaChannel).toBeFalsy();
28+
});
29+
it("should return alphachannel = false for <= 3 channels", () => {
30+
const SUT = new Image(200, 200, 123, 1, "id");
31+
expect(SUT.hasAlphaChannel).toBeFalsy();
32+
});
33+
34+
it("should throw for <= 0 channels", () => {
35+
expect(() => new Image(200, 200, 123, 0, "id")).toThrowError("Channel <= 0");
36+
});
37+
38+
it("should have a default pixel density of 1.0", () => {
39+
const SUT = new Image(200, 200, 123, 1, "id");
40+
expect(SUT.pixelDensity).toEqual({scaleX: 1.0, scaleY: 1.0});
41+
});
42+
43+
describe("Colormode", () => {
44+
it("should not try to convert an image to BGR if it already has the correct color mode", async () => {
45+
// GIVEN
46+
const bgrImage = new Image(100, 100, Buffer.from([]), 3, "testImage");
47+
48+
// WHEN
49+
const convertedImage = await bgrImage.toBGR();
50+
51+
// THEN
52+
expect(convertedImage).toBe(bgrImage);
53+
expect(imageToJimp).not.toBeCalledTimes(1)
54+
});
55+
56+
it("should not try to convert an image to RGB if it already has the correct color mode", async () => {
57+
// GIVEN
58+
const rgbImage = new Image(100, 100, Buffer.from([]), 3, "testImage", ColorMode.RGB);
59+
60+
// WHEN
61+
const convertedImage = await rgbImage.toRGB();
62+
63+
// THEN
64+
expect(convertedImage).toBe(rgbImage);
65+
expect(imageToJimp).not.toBeCalledTimes(1)
66+
});
67+
});
3068
});

lib/image.class.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import {imageToJimp} from "./provider/io/imageToJimp.function";
2+
import {ColorMode} from "./colormode.enum";
3+
14
/**
25
* The {@link Image} class represents generic image data
36
*/
@@ -9,6 +12,7 @@ export class Image {
912
* @param data Generic {@link Image} data
1013
* @param channels Amount of {@link Image} channels
1114
* @param id Image identifier
15+
* @param colorMode An images color mode, defaults to {@link ColorMode.BGR}
1216
* @param pixelDensity Object containing scale info to work with e.g. Retina display data where the reported display size and pixel size differ (Default: {scaleX: 1.0, scaleY: 1.0})
1317
*/
1418
constructor(
@@ -17,6 +21,7 @@ export class Image {
1721
public readonly data: any,
1822
public readonly channels: number,
1923
public readonly id: string,
24+
public readonly colorMode: ColorMode = ColorMode.BGR,
2025
public readonly pixelDensity: { scaleX: number; scaleY: number } = {
2126
scaleX: 1.0,
2227
scaleY: 1.0,
@@ -33,4 +38,35 @@ export class Image {
3338
public get hasAlphaChannel() {
3439
return this.channels > 3;
3540
}
41+
42+
/**
43+
* {@link toRGB} converts an {@link Image} from BGR color mode (default within nut.js) to RGB
44+
*/
45+
public async toRGB(): Promise<Image> {
46+
if (this.colorMode === ColorMode.RGB) {
47+
return this;
48+
}
49+
const rgbImage = imageToJimp(this);
50+
return new Image(this.width, this.height, rgbImage.bitmap.data, this.channels, this.id, ColorMode.RGB, this.pixelDensity);
51+
}
52+
53+
/**
54+
* {@link toBGR} converts an {@link Image} from RGB color mode to RGB
55+
*/
56+
public async toBGR(): Promise<Image> {
57+
if (this.colorMode === ColorMode.BGR) {
58+
return this;
59+
}
60+
const rgbImage = imageToJimp(this);
61+
return new Image(this.width, this.height, rgbImage.bitmap.data, this.channels, this.id, ColorMode.BGR, this.pixelDensity);
62+
}
63+
64+
/**
65+
* {@link fromRGBData} creates an {@link Image} from provided RGB data
66+
*/
67+
public static fromRGBData(width: number, height: number, data: Buffer, channels: number, id: string): Image {
68+
const rgbImage = new Image(width, height, data, channels, id);
69+
const jimpImage = imageToJimp(rgbImage);
70+
return new Image(width, height, jimpImage.bitmap.data, channels, id);
71+
}
3672
}

lib/match-request.class.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {Image} from "./image.class";
22
import {MatchRequest} from "./match-request.class";
33

4+
jest.mock('jimp', () => {});
5+
46
describe("MatchRequest", () => {
57
it("should default to multi-scale matching", () => {
68
const SUT = new MatchRequest(
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import Jimp from "jimp";
22
import {Image} from "../../image.class";
3+
import {ColorMode} from "../../colormode.enum";
34

45
export function imageToJimp(image: Image): Jimp {
56
const jimpImage = new Jimp({
67
data: image.data,
78
width: image.width,
89
height: image.height
910
});
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-
});
11+
if (image.colorMode === ColorMode.BGR) {
12+
// Image treats 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+
});
18+
}
1619
return jimpImage;
1720
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Jimp from 'jimp';
22
import {ImageReader} from "../image-reader.type";
33
import {Image} from "../../image.class";
4+
import {ColorMode} from "../../colormode.enum";
45

56
export default class implements ImageReader {
67
load(parameters: string): Promise<Image> {
@@ -18,7 +19,8 @@ export default class implements ImageReader {
1819
jimpImage.bitmap.height,
1920
jimpImage.bitmap.data,
2021
jimpImage.hasAlpha() ? 4 : 3,
21-
parameters
22+
parameters,
23+
ColorMode.BGR
2224
));
2325
}).catch(err => reject(`Failed to load image from '${parameters}'. Reason: ${err}`));
2426
})

lib/provider/native/libnut-screen.class.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import libnut = require("@nut-tree/libnut");
22
import { Region } from "../../region.class";
33
import ScreenAction from "./libnut-screen.class";
44

5+
jest.mock("jimp", () => {});
56
jest.mock("@nut-tree/libnut");
67

78
beforeEach(() => {

lib/provider/native/libnut-screen.class.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import libnut = require("@nut-tree/libnut");
2-
import { Image } from "../../image.class";
3-
import { Region } from "../../region.class";
4-
import { ScreenProviderInterface } from "../screen-provider.interface";
2+
import {Image} from "../../image.class";
3+
import {Region} from "../../region.class";
4+
import {ScreenProviderInterface} from "../screen-provider.interface";
5+
import {ColorMode} from "../../colormode.enum";
56

67
export default class ScreenAction implements ScreenProviderInterface {
78

@@ -34,6 +35,7 @@ export default class ScreenAction implements ScreenProviderInterface {
3435
screenShot.image,
3536
4,
3637
"grabScreenResult",
38+
ColorMode.BGR,
3739
pixelScaling,
3840
),
3941
);
@@ -60,6 +62,7 @@ export default class ScreenAction implements ScreenProviderInterface {
6062
screenShot.image,
6163
4,
6264
"grabScreenRegionResult",
65+
ColorMode.BGR,
6366
pixelScaling,
6467
),
6568
);

0 commit comments

Comments
 (0)