adds testing & updates readme

This commit is contained in:
JasterV 2022-03-21 15:35:37 +01:00
parent 027d91ddcc
commit 1ee123ce72
7 changed files with 7245 additions and 63 deletions

View file

@ -1,6 +1,6 @@
<h1 align="center">Welcome to imgphash 👋</h1>
<p>
<img alt="Version" src="https://img.shields.io/badge/version-0.1.0-blue.svg?cacheSeconds=2592000" />
<img alt="Version" src="https://img.shields.io/badge/version-0.2.0-blue.svg?cacheSeconds=2592000" />
<a href="https://github.com/JasterV/imgphash#readme" target="_blank">
<img alt="Documentation" src="https://img.shields.io/badge/documentation-yes-brightgreen.svg" />
</a>
@ -43,11 +43,13 @@ const image = new HashImage(buffer)
```javascript
const image1 = await HashImage.fromUrl(url1);
const image2 = await HashImage.fromUrl(url2);
const hash1 = await image1.hash()
const hash1 = await image1.hash() // PHash instance
const hash2 = await image2.hash()
const similarity = HashImage.hashCompare(hash1, hash2)
const similarity = hash1.compare(hash2)
```
> The hash function returns an instance of `PHash`
+ Or just compare 2 image objects, this is going to internally calculate their hash and use it
```javascript
@ -77,7 +79,7 @@ Give a ⭐️ if this project helped you!
## 📝 License
Copyright © 2022 [Victor Martinez <jaster.victor@gmail.com>](https://github.com/JasterV).<br />
This project is [MIT](https://choosealicense.com/licenses/mit/) licensed.
This project is [MIT](https://github.com/JasterV/imgphash/blob/main/LICENSE) licensed.
***
_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_

7044
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,13 +11,18 @@
],
"author": "Victor Martinez <jaster.victor@gmail.com>",
"license": "MIT",
"scripts": {
"test": "jest"
},
"dependencies": {
"@canvas/image": "^1.0.1",
"axios": "^0.26.1",
"blockhash-core": "^0.1.0"
},
"devDependencies": {
"@types/node": "^17.0.21"
"@types/jest": "^27.4.1",
"@types/node": "^17.0.21",
"jest": "^27.5.1"
},
"repository": {
"type": "git",
@ -27,4 +32,4 @@
"url": "https://github.com/JasterV/imgphash/issues"
},
"homepage": "https://github.com/JasterV/imgphash#readme"
}
}

44
src/HashImage.js Normal file
View file

@ -0,0 +1,44 @@
const { imageFromBuffer, getImageData } = require("@canvas/image");
const PHash = require("./PHash");
const { download, hexToBin } = require("./utils");
const blockhash = require("blockhash-core");
class HashImage {
constructor(buffer) {
if (!(buffer instanceof Uint8Array)) {
throw new Error(
"Invalid parameter, please use a buffer or an instance of Uint8Array"
);
}
this.buffer = buffer;
}
static async fromUrl(url) {
try {
const buffer = await download(url);
return new HashImage(buffer);
} catch (err) {
throw new Error(
"Error on image download, make sure you are passing a valid string url"
);
}
}
async hash() {
const data = await imageFromBuffer(this.buffer);
const hexHash = await blockhash.bmvbhash(getImageData(data), 8);
const hash = hexToBin(hexHash);
return new PHash(hash);
}
async compare(other) {
if (!(other instanceof HashImage)) {
throw new Error("Can't compare with a non HashImage value");
}
const hash1 = await this.hash();
const hash2 = await other.hash();
return hash1.compare(hash2);
}
}
module.exports = HashImage;

36
src/PHash.js Normal file
View file

@ -0,0 +1,36 @@
class PHash {
constructor(hash) {
if (typeof hash !== "string") {
throw new Error(
"Can't construct a PHash instance with a non-string value"
);
}
const isNonBinary = hash
.trim()
.split("")
.some((chr) => chr !== "0" && chr !== "1");
if (isNonBinary) {
throw new Error(
"Can't construct a PHash instance with a non-binary string value"
);
}
this.hash = hash;
}
compare(other) {
if (!(other instanceof PHash)) {
throw new Error("Can't compare with a non PHash value");
}
const minLength = Math.min(this.hash.length, other.hash.length);
const maxLength = Math.max(this.hash.length, other.hash.length);
let similarity = 0;
for (let i = 0; i < minLength; i++) {
if (this.hash[i] === other.hash[i]) {
similarity += 1;
}
}
return similarity / maxLength;
}
}
module.exports = PHash;

View file

@ -1,55 +1,4 @@
const blockhash = require("blockhash-core");
const { getImageData, imageFromBuffer } = require("@canvas/image");
const { hexToBin, download } = require("./utils");
const HashImage = require("./HashImage");
const PHash = require("./PHash");
class HashImage {
constructor(buffer) {
if (!(buffer instanceof Uint8Array)) {
throw new Error(
"Invalid parameter, please use a buffer or an instance of Uint8Array"
);
}
this.buffer = buffer;
}
static async fromUrl(url) {
try {
const buffer = await download(url);
return new HashImage(buffer);
} catch (err) {
throw new Error(
"Error on image download, make sure you are passing a valid string url"
);
}
}
static hashCompare(hash1, hash2) {
if (typeof hash1 !== "string" || typeof hash2 !== "string") {
throw new Error("Both hash values need to be strings");
}
let similarity = 0;
const hash1Array = hash1.split("");
hash1Array.forEach((bit, index) => {
hash2[index] === bit ? similarity++ : null;
});
return similarity / hash1.length;
}
async hash() {
const data = await imageFromBuffer(this.buffer);
const hexHash = await blockhash.bmvbhash(getImageData(data), 8);
const hash = hexToBin(hexHash);
return hash;
}
async compare(other) {
if (!(other instanceof HashImage)) {
throw new Error("Can't compare with a non HashImage value");
}
const hash1 = await this.hash();
const hash2 = await other.hash();
return HashImage.hashCompare(hash1, hash2);
}
}
module.exports = { HashImage };
module.exports = { HashImage, PHash };

108
test/index.test.js Normal file
View file

@ -0,0 +1,108 @@
const HashImage = require("../src/HashImage");
const PHash = require("../src/PHash");
const url1 =
"https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/w_400/koala1.jpg";
const url2 = "https://res.cloudinary.com/demo/image/upload/h_180/koala2.jpg";
const url3 =
"https://res.cloudinary.com/demo/image/upload/h_180/another_koala.jpg";
const hash1 = "010101";
const hash2 = "010001";
const testBuffer = Buffer.from("Hello, World");
describe("Test HashImage", () => {
it("Can create an instance from a buffer", async () => {
expect.assertions(1);
const image = new HashImage(testBuffer);
expect(image).toBeInstanceOf(HashImage);
});
it("Can create an instance from a valid url", async () => {
expect.assertions(1);
const image = await HashImage.fromUrl(url1);
expect(image).toBeInstanceOf(HashImage);
});
it("Can't create an instance from an invalid url", async () => {
expect.assertions(1);
await expect(HashImage.fromUrl("invalid-url")).rejects.toThrowError(
"Error on image download, make sure you are passing a valid string url"
);
});
it("Can compare 2 valid images", async () => {
expect.assertions(1);
const image1 = await HashImage.fromUrl(url1);
const image2 = await HashImage.fromUrl(url2);
await expect(image1.compare(image2)).resolves.toEqual(expect.any(Number));
});
it("Returns 1 when comparing 2 equal images", async () => {
expect.assertions(1);
const image1 = await HashImage.fromUrl(url1);
const image2 = await HashImage.fromUrl(url1);
const similarity = await image1.compare(image2);
expect(similarity).toBe(1);
});
it("Can't compare with a non HashImage object", async () => {
expect.assertions(1);
const image = new HashImage(testBuffer);
await expect(image.compare("asdfsdf")).rejects.toThrowError(
"Can't compare with a non HashImage value"
);
});
});
describe("Test PHash", () => {
it("Can create an instance from a valid binary hash", () => {
expect.assertions(1);
const phash = new PHash(hash1);
expect(phash).toBeInstanceOf(PHash);
});
it("Can't create an instance from a non string value", () => {
expect.assertions(1);
try {
new PHash(2);
} catch (err) {
expect(err.message).toBe(
"Can't construct a PHash instance with a non-string value"
);
}
});
it("Can't create an instance from a non binary hash", () => {
expect.assertions(1);
try {
new PHash("dfgdfg");
} catch (err) {
expect(err.message).toBe(
"Can't construct a PHash instance with a non-binary string value"
);
}
});
it("Can compare 2 valid PHash", () => {
expect.assertions(1);
const phash1 = new PHash(hash1);
const phash2 = new PHash(hash2);
const result = phash1.compare(phash2);
expect(result).toEqual(expect.any(Number));
});
it("Returns 1 for 2 completely equal hashes", () => {
expect.assertions(1);
const phash1 = new PHash(hash1);
const result = phash1.compare(phash1);
expect(result).toBe(1);
});
it("Returns a result smaller than 1 for 2 different hashes", () => {
expect.assertions(1);
const phash1 = new PHash(hash1);
const phash2 = new PHash(hash2);
const similarity = phash1.compare(phash2);
expect(similarity).toBeLessThan(1);
});
});