Skip to content

Commit 4363c90

Browse files
feat: enhance classifyImage functionality to support optional hash computation and update related types
1 parent 00e8b2a commit 4363c90

6 files changed

Lines changed: 114 additions & 28 deletions

File tree

__tests__/unit/448x448.jpg

4.53 KB
Loading

__tests__/unit/hashing.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, it,} from 'vitest';
2+
import { computeHashesFromStream, HashType, ImageFormat, RequestEncoding } from '../../src';
3+
import fs from 'fs';
4+
5+
describe('hashing', () => {
6+
it('should compute MD5 and SHA1 hashes for a valid image', async ({expect}) => {
7+
// Test implementation here
8+
// Test implementation here
9+
const imagePath = __dirname + '/448x448.jpg';
10+
11+
const imageStream = fs.createReadStream(imagePath);
12+
13+
//assert error is thrown for invalid image
14+
const { data, format, md5, sha1 } = await computeHashesFromStream(imageStream,
15+
RequestEncoding.UNCOMPRESSED,
16+
ImageFormat.JPEG,
17+
false,
18+
[HashType.MD5, HashType.SHA1]
19+
);
20+
21+
expect(data).toBeDefined();
22+
expect(format).toBe(ImageFormat.JPEG);
23+
24+
expect(md5).toEqual('eb3a95fdd86ce28d9a63a68328783874');
25+
expect(sha1).toEqual('b972b222bc91c457d904ebff16134dc79b67d1c9');
26+
});
27+
28+
it('should compute MD5 and SHA1 hashes for a resized invalid image', async ({expect}) => {
29+
// Test implementation here
30+
// Test implementation here
31+
const imagePath = __dirname + '/578x478.jpg';
32+
33+
const imageStream = fs.createReadStream(imagePath);
34+
35+
//assert error is thrown for invalid image
36+
const { data, format, md5, sha1 } = await computeHashesFromStream(imageStream,
37+
RequestEncoding.UNCOMPRESSED,
38+
ImageFormat.JPEG,
39+
true,
40+
[HashType.MD5, HashType.SHA1]
41+
);
42+
43+
expect(data).toBeDefined();
44+
expect(format).toBe(ImageFormat.RAW_UINT8);
45+
46+
expect(md5).toEqual('d390b6cd7436fd41d1bcd005e7e3e652');
47+
expect(sha1).toEqual('7f979ca35cfe390bd92f5dfa4a05919da76f0e43');
48+
});
49+
50+
it('should throw an error for an invalid image', async ({expect}) => {
51+
// Test implementation here
52+
const imagePath = __dirname + '/578x478.jpg';
53+
54+
const imageStream = fs.createReadStream(imagePath);
55+
56+
//assert error is thrown for invalid image
57+
await expect(computeHashesFromStream(imageStream)).rejects.toThrow('Image must be 448x448 pixels');
58+
});
59+
});

__tests__/unit/main.test.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { ClassificationOutput, ClassifierSdk, ClassifyImageInput, ImageFormat }
33
import fs from 'fs';
44
import { randomUUID } from 'crypto';
55

6-
describe('classifierHelper', () => {
6+
describe('ClassifierSdk smoke tests', () => {
7+
78
it('should listDeployments and return responses (smoke test)', async ({expect}) => {
89
// This is a smoke test. You must have a running gRPC server at localhost:50051 for this to pass.
910
// You may want to mock the gRPC client for true unit testing.
@@ -34,7 +35,7 @@ describe('classifyImage function', () => {
3435
it('should classify 10 images in a single request and return responses (integration smoke test)', async ({expect, annotate}) => {
3536
// This is a smoke test. You must have a running gRPC server at localhost:50051 for this to pass.
3637
// You may want to mock the gRPC client for true unit testing.
37-
const imagePath = __dirname + '/Steamboat-willie.jpg';
38+
const imagePath = __dirname + '/448x448.jpg';
3839
const sdk = new ClassifierSdk({
3940
deploymentId: process.env.VITE_ATHENA_DEPLOYMENT_ID,
4041
affiliate: process.env.VITE_ATHENA_AFFILIATE,
@@ -111,22 +112,21 @@ describe('classifyImage function', () => {
111112
const expectedOutputs = correlationIds.map(id =>(
112113
{
113114
correlationId: id,
114-
classifications: expect.arrayContaining([
115+
classifications: expect.toBeOneOf([expect.arrayContaining([
115116
{
116117
label: expect.any(String),
117118
weight: expect.any(Number)
118119
}
119-
])
120+
]), []])
120121
}
121122
));
122123

123124
expect(outputs).toMatchObject(expectedOutputs);
124125
}, 120000);
125126

126-
it('should classify Steamboat-willie.jpg with raw uint8 resize return responses (integration smoke test)', async ({expect, annotate}) => {
127-
// This is a smoke test. You must have a running gRPC server at localhost:50051 for this to pass.
128-
// You may want to mock the gRPC client for true unit testing.
129-
const imagePath = __dirname + '/Steamboat-willie.jpg';
127+
it('should classify with raw uint8 resize return responses (integration smoke test)', async ({expect, annotate}) => {
128+
129+
const imagePath = __dirname + '/448x448.jpg';
130130
const sdk = new ClassifierSdk({
131131
deploymentId: process.env.VITE_ATHENA_DEPLOYMENT_ID,
132132
affiliate: process.env.VITE_ATHENA_AFFILIATE,
@@ -187,23 +187,23 @@ describe('classifyImage function', () => {
187187
expect(first).toMatchObject([
188188
{
189189
correlationId,
190-
classifications: expect.arrayContaining([
190+
classifications: expect.toBeOneOf([expect.arrayContaining([
191191
{
192192
label: expect.any(String),
193193
weight: expect.any(Number)
194194
}
195-
])
195+
]), []])
196196
} as ClassificationOutput
197197
]);
198198

199199
// Accept either a successful call or a connection error (for CI/dev convenience)
200200
expect(error).toBeUndefined();
201201
}, 120000);
202202

203-
it('should classify Steamboat-willie.jpg and return responses (integration smoke test)', async ({expect, annotate}) => {
203+
it('should classify return responses (integration smoke test)', async ({expect, annotate}) => {
204204
// This is a smoke test. You must have a running gRPC server at localhost:50051 for this to pass.
205205
// You may want to mock the gRPC client for true unit testing.
206-
const imagePath = __dirname + '/Steamboat-willie.jpg';
206+
const imagePath = __dirname + '/448x448.jpg';
207207
const sdk = new ClassifierSdk({
208208
deploymentId: process.env.VITE_ATHENA_DEPLOYMENT_ID,
209209
affiliate: process.env.VITE_ATHENA_AFFILIATE,
@@ -248,7 +248,7 @@ describe('classifyImage function', () => {
248248
const options: ClassifyImageInput = {
249249
imageStream,
250250
correlationId,
251-
format: ImageFormat.JPEG
251+
format: ImageFormat.JPEG,
252252
};
253253
try {
254254
await sdk.sendClassifyRequest(options);
@@ -264,12 +264,12 @@ describe('classifyImage function', () => {
264264
expect(first).toMatchObject([
265265
{
266266
correlationId,
267-
classifications: expect.arrayContaining([
267+
classifications: expect.toBeOneOf([expect.arrayContaining([
268268
{
269269
label: expect.any(String),
270270
weight: expect.any(Number)
271271
}
272-
])
272+
]), []])
273273
} as ClassificationOutput
274274
]);
275275

src/hashing.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Readable } from 'stream';
22
import crypto from 'crypto';
33
import sharp from 'sharp';
4-
import { ImageFormat, RequestEncoding } from '.';
4+
import { HashType, ImageFormat, RequestEncoding } from '.';
55
import brotli from 'brotli';
66
import { buffer } from 'stream/consumers';
77

@@ -10,39 +10,55 @@ import { buffer } from 'stream/consumers';
1010
* @param stream Node.js readable stream (e.g., fs.createReadStream)
1111
* @param encoding Encoding type for the image data (default is UNCOMPRESSED)
1212
* @param imageFormat Format of the input image (default is UNSPECIFIED)
13-
* @param resize Whether to resize the image to 448x448 pixels (default is false
14-
* @returns {Promise<{md5: string, sha1: string, data: Buffer}>} Object containing MD5 hash, SHA1 hash, and resized image buffer
13+
* @param resize Whether to resize the image to 448x448 pixels (default is false)
14+
* @param hashes Array of hash types to compute (default is [MD5, SHA1])
15+
* @returns {Promise<{md5?: string, sha1?: string, data: Buffer}>} Object containing MD5 hash, SHA1 hash, and resized image buffer
1516
*/
1617
export async function computeHashesFromStream(
1718
stream: Readable,
1819
encoding: RequestEncoding = RequestEncoding.UNCOMPRESSED,
1920
imageFormat: ImageFormat = ImageFormat.UNSPECIFIED,
2021
resize: boolean = false,
21-
): Promise<{ md5: string; sha1: string; data: Buffer; format: ImageFormat }> {
22+
hashes: HashType[] = [HashType.MD5, HashType.SHA1],
23+
): Promise<{ md5?: string; sha1?: string; data: Buffer; format: ImageFormat }> {
2224
const md5 = crypto.createHash('md5');
2325
const sha1 = crypto.createHash('sha1');
2426

2527
let data: Buffer<ArrayBufferLike>;
2628

27-
stream.pipe(md5);
28-
stream.pipe(sha1);
29+
if (hashes.includes(HashType.MD5)) {
30+
{
31+
stream.pipe(md5);
32+
}
33+
}
34+
35+
if (hashes.includes(HashType.SHA1)) {
36+
{
37+
stream.pipe(sha1);
38+
}
39+
}
2940

3041
if (resize) {
31-
const resizer = sharp().resize(448, 448).raw({ depth: 'uint' });
42+
const resizer = sharp().resize(448, 448).raw({ depth: 'char' });
3243
stream.pipe(resizer);
3344
data = await resizer.toBuffer();
3445
imageFormat = ImageFormat.RAW_UINT8;
3546
} else {
3647
data = await buffer(stream);
48+
// use sharp to validate the image dimensions
49+
const metadata = await sharp(data).metadata();
50+
if (metadata.width !== 448 || metadata.height !== 448) {
51+
throw new Error('Image must be 448x448 pixels');
52+
}
3753
}
3854

3955
if (encoding === RequestEncoding.BROTLI) {
4056
data = Buffer.from(await brotli.compress(data));
4157
}
4258

4359
return {
44-
md5: md5.digest('hex'),
45-
sha1: sha1.digest('hex'),
60+
md5: hashes.includes(HashType.MD5) ? md5.digest('hex') : undefined,
61+
sha1: hashes.includes(HashType.SHA1) ? sha1.digest('hex') : undefined,
4662
data,
4763
format: imageFormat,
4864
};

src/index.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@ import { computeHashesFromStream } from './hashing';
3131
* @property imageStream The image data as a readable stream.
3232
* @property encoding Optional encoding type for the image data.
3333
* @property format Optional image format.
34+
* @property resize Optional flag to resize the image.
35+
* @property hashes Array of hash types to compute.
3436
*/
3537
export type ClassifyImageInput = {
3638
affiliate?: string;
3739
correlationId?: string;
3840
imageStream: Readable;
3941
encoding?: ClassifyRequest['inputs'][number]['encoding'];
42+
includeHashes?: HashType[];
4043
} & (ResizeImageInput | RawImageInput);
4144

4245
export type ResizeImageInput = {
@@ -246,6 +249,7 @@ export class ClassifierSdk extends (EventEmitter as new () => TypedEmitter<Class
246249
affiliate = this.options.affiliate,
247250
correlationId = randomUUID(),
248251
imageStream,
252+
includeHashes = [HashType.MD5, HashType.SHA1],
249253
encoding = RequestEncoding.UNCOMPRESSED,
250254
} = options;
251255

@@ -260,11 +264,18 @@ export class ClassifierSdk extends (EventEmitter as new () => TypedEmitter<Class
260264
encoding,
261265
inputFormat,
262266
'resize' in options,
267+
includeHashes,
263268
);
264-
const hashes: ImageHash[] = [
265-
{ value: md5, type: HashType.MD5 },
266-
{ value: sha1, type: HashType.SHA1 },
267-
];
269+
270+
const hashes: ImageHash[] = [];
271+
272+
if (md5 && md5.trim() != '') {
273+
hashes.push({ value: md5, type: HashType.MD5 });
274+
}
275+
276+
if (sha1 && sha1.trim() != '') {
277+
hashes.push({ value: sha1, type: HashType.SHA1 });
278+
}
268279

269280
processedInputs.push({
270281
affiliate,

0 commit comments

Comments
 (0)