mirror of
https://codeberg.org/JasterV/imgphash.git
synced 2026-04-26 18:10:01 +00:00
adds testing & updates readme
This commit is contained in:
parent
027d91ddcc
commit
1ee123ce72
7 changed files with 7245 additions and 63 deletions
10
README.md
10
README.md
|
|
@ -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
7044
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
44
src/HashImage.js
Normal 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
36
src/PHash.js
Normal 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;
|
||||
57
src/lib.js
57
src/lib.js
|
|
@ -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
108
test/index.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue