Skip to content

Feature/336/colormode conversion #337

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 7 commits into from
Dec 10, 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
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export {Point} from "./lib/point.class";
export {Region} from "./lib/region.class";
export {Window} from "./lib/window.class";
export {FileType} from "./lib/file-type.enum";
export {ColorMode} from "./lib/colormode.enum";

const lineHelper = new LineHelper();

Expand Down
7 changes: 7 additions & 0 deletions lib/colormode.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* The {@link ColorMode} enum is used to specify the color mode of an {@link Image}
*/
export enum ColorMode {
BGR,
RGB
}
92 changes: 65 additions & 27 deletions lib/image.class.spec.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,68 @@
import { Image } from "./image.class";
import {Image} from "./image.class";
import {imageToJimp} from "./provider/io/imageToJimp.function";
import {ColorMode} from "./colormode.enum";

jest.mock("./provider/io/imageToJimp.function", () => {
return {
imageToJimp: jest.fn()
}
});

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

describe("Image class", () => {
it("should return alphachannel = true for > 3 channels", () => {
const SUT = new Image(200, 200, 123, 4, "id");
expect(SUT.hasAlphaChannel).toBeTruthy();
});

it("should return alphachannel = false for <= 3 channels", () => {
const SUT = new Image(200, 200, 123, 3, "id");
expect(SUT.hasAlphaChannel).toBeFalsy();
});
it("should return alphachannel = false for <= 3 channels", () => {
const SUT = new Image(200, 200, 123, 2, "id");
expect(SUT.hasAlphaChannel).toBeFalsy();
});
it("should return alphachannel = false for <= 3 channels", () => {
const SUT = new Image(200, 200, 123, 1, "id");
expect(SUT.hasAlphaChannel).toBeFalsy();
});

it("should throw for <= 0 channels", () => {
expect(() => new Image(200, 200, 123, 0, "id")).toThrowError("Channel <= 0");
});

it("should have a default pixel density of 1.0", () => {
const SUT = new Image(200, 200, 123, 1, "id");
expect(SUT.pixelDensity).toEqual({ scaleX: 1.0, scaleY: 1.0 });
});
it("should return alphachannel = true for > 3 channels", () => {
const SUT = new Image(200, 200, 123, 4, "id");
expect(SUT.hasAlphaChannel).toBeTruthy();
});

it("should return alphachannel = false for <= 3 channels", () => {
const SUT = new Image(200, 200, 123, 3, "id");
expect(SUT.hasAlphaChannel).toBeFalsy();
});
it("should return alphachannel = false for <= 3 channels", () => {
const SUT = new Image(200, 200, 123, 2, "id");
expect(SUT.hasAlphaChannel).toBeFalsy();
});
it("should return alphachannel = false for <= 3 channels", () => {
const SUT = new Image(200, 200, 123, 1, "id");
expect(SUT.hasAlphaChannel).toBeFalsy();
});

it("should throw for <= 0 channels", () => {
expect(() => new Image(200, 200, 123, 0, "id")).toThrowError("Channel <= 0");
});

it("should have a default pixel density of 1.0", () => {
const SUT = new Image(200, 200, 123, 1, "id");
expect(SUT.pixelDensity).toEqual({scaleX: 1.0, scaleY: 1.0});
});

describe("Colormode", () => {
it("should not try to convert an image to BGR if it already has the correct color mode", async () => {
// GIVEN
const bgrImage = new Image(100, 100, Buffer.from([]), 3, "testImage");

// WHEN
const convertedImage = await bgrImage.toBGR();

// THEN
expect(convertedImage).toBe(bgrImage);
expect(imageToJimp).not.toBeCalledTimes(1)
});

it("should not try to convert an image to RGB if it already has the correct color mode", async () => {
// GIVEN
const rgbImage = new Image(100, 100, Buffer.from([]), 3, "testImage", ColorMode.RGB);

// WHEN
const convertedImage = await rgbImage.toRGB();

// THEN
expect(convertedImage).toBe(rgbImage);
expect(imageToJimp).not.toBeCalledTimes(1)
});
});
});
36 changes: 36 additions & 0 deletions lib/image.class.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import {imageToJimp} from "./provider/io/imageToJimp.function";
import {ColorMode} from "./colormode.enum";

/**
* The {@link Image} class represents generic image data
*/
Expand All @@ -9,6 +12,7 @@ export class Image {
* @param data Generic {@link Image} data
* @param channels Amount of {@link Image} channels
* @param id Image identifier
* @param colorMode An images color mode, defaults to {@link ColorMode.BGR}
* @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})
*/
constructor(
Expand All @@ -17,6 +21,7 @@ export class Image {
public readonly data: any,
public readonly channels: number,
public readonly id: string,
public readonly colorMode: ColorMode = ColorMode.BGR,
public readonly pixelDensity: { scaleX: number; scaleY: number } = {
scaleX: 1.0,
scaleY: 1.0,
Expand All @@ -33,4 +38,35 @@ export class Image {
public get hasAlphaChannel() {
return this.channels > 3;
}

/**
* {@link toRGB} converts an {@link Image} from BGR color mode (default within nut.js) to RGB
*/
public async toRGB(): Promise<Image> {
if (this.colorMode === ColorMode.RGB) {
return this;
}
const rgbImage = imageToJimp(this);
return new Image(this.width, this.height, rgbImage.bitmap.data, this.channels, this.id, ColorMode.RGB, this.pixelDensity);
}

/**
* {@link toBGR} converts an {@link Image} from RGB color mode to RGB
*/
public async toBGR(): Promise<Image> {
if (this.colorMode === ColorMode.BGR) {
return this;
}
const rgbImage = imageToJimp(this);
return new Image(this.width, this.height, rgbImage.bitmap.data, this.channels, this.id, ColorMode.BGR, this.pixelDensity);
}

/**
* {@link fromRGBData} creates an {@link Image} from provided RGB data
*/
public static fromRGBData(width: number, height: number, data: Buffer, channels: number, id: string): Image {
const rgbImage = new Image(width, height, data, channels, id);
const jimpImage = imageToJimp(rgbImage);
return new Image(width, height, jimpImage.bitmap.data, channels, id);
}
}
2 changes: 2 additions & 0 deletions lib/match-request.class.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {Image} from "./image.class";
import {MatchRequest} from "./match-request.class";

jest.mock('jimp', () => {});

describe("MatchRequest", () => {
it("should default to multi-scale matching", () => {
const SUT = new MatchRequest(
Expand Down
15 changes: 9 additions & 6 deletions lib/provider/io/imageToJimp.function.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import Jimp from "jimp";
import {Image} from "../../image.class";
import {ColorMode} from "../../colormode.enum";

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;
});
if (image.colorMode === ColorMode.BGR) {
// Image treats 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;
}
4 changes: 3 additions & 1 deletion lib/provider/io/jimp-image-reader.class.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Jimp from 'jimp';
import {ImageReader} from "../image-reader.type";
import {Image} from "../../image.class";
import {ColorMode} from "../../colormode.enum";

export default class implements ImageReader {
load(parameters: string): Promise<Image> {
Expand All @@ -18,7 +19,8 @@ export default class implements ImageReader {
jimpImage.bitmap.height,
jimpImage.bitmap.data,
jimpImage.hasAlpha() ? 4 : 3,
parameters
parameters,
ColorMode.BGR
));
}).catch(err => reject(`Failed to load image from '${parameters}'. Reason: ${err}`));
})
Expand Down
1 change: 1 addition & 0 deletions lib/provider/native/libnut-screen.class.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import libnut = require("@nut-tree/libnut");
import { Region } from "../../region.class";
import ScreenAction from "./libnut-screen.class";

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

beforeEach(() => {
Expand Down
9 changes: 6 additions & 3 deletions lib/provider/native/libnut-screen.class.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import libnut = require("@nut-tree/libnut");
import { Image } from "../../image.class";
import { Region } from "../../region.class";
import { ScreenProviderInterface } from "../screen-provider.interface";
import {Image} from "../../image.class";
import {Region} from "../../region.class";
import {ScreenProviderInterface} from "../screen-provider.interface";
import {ColorMode} from "../../colormode.enum";

export default class ScreenAction implements ScreenProviderInterface {

Expand Down Expand Up @@ -34,6 +35,7 @@ export default class ScreenAction implements ScreenProviderInterface {
screenShot.image,
4,
"grabScreenResult",
ColorMode.BGR,
pixelScaling,
),
);
Expand All @@ -60,6 +62,7 @@ export default class ScreenAction implements ScreenProviderInterface {
screenShot.image,
4,
"grabScreenRegionResult",
ColorMode.BGR,
pixelScaling,
),
);
Expand Down