Skip to content
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
3 changes: 2 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@resize-it/api",
"version": "1.1.1",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "bun test",
"dev": "bun run --watch src/index.ts",
"build": "bun build src/index.ts --outdir dist",
"start": "bun run dist/index.js"
Expand All @@ -12,6 +12,7 @@
"@elysiajs/static": "^1.2.0",
"@elysiajs/swagger": "^1.2.2",
"elysia": "latest",
"heic-convert": "^2.1.0",
"ioredis": "^5.5.0",
"minio": "^8.0.4",
"sharp": "^0.33.5"
Expand Down
43 changes: 42 additions & 1 deletion packages/api/src/services/image.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ interface CachedImage {
export class ImageService {
private monitoringService?: MonitoringService;
private cacheService?: CacheService;
private heicConvert?: (options: { buffer: Buffer; format: "JPEG" | "PNG" }) => Promise<ArrayBuffer>;

constructor(
monitoringService?: MonitoringService,
Expand All @@ -57,13 +58,53 @@ export class ImageService {
this.cacheService = cacheService;
}

private isHeic(buffer: Buffer): boolean {
if (buffer.length < 12) {
return false;
}
const signature = buffer.subarray(4, 12).toString();
const validSignatures = [
"ftypheic",
"ftypheix",
"ftyphevc",
"ftyphevx",
"ftypmif1",
"ftypmsf1",
];
if (validSignatures.includes(signature)) {
return true;
}

const signatureShort = signature.slice(4, 8);
const validShortSignatures = ["heic", "heix", "hevc", "hevx"];

return validShortSignatures.includes(signatureShort);
}

async resize(
imageBuffer: Buffer,
options: ResizeOptions,
originalPath: string = "unknown"
): Promise<Buffer> {
const startTime = performance.now();
const inputSize = imageBuffer.length;
let processedImageBuffer = imageBuffer;

if (this.isHeic(imageBuffer)) {
try {
if (!this.heicConvert) {
this.heicConvert = (await import("heic-convert")).default;
}
processedImageBuffer = Buffer.from(
await this.heicConvert({
buffer: imageBuffer,
format: "JPEG",
})
);
} catch (error) {
throw new Error(`Failed to convert HEIC image: ${(error as Error).message}`);
}
}

// Try to get from cache first if cache service is available
if (this.cacheService && config.cache.enabled) {
Expand Down Expand Up @@ -101,7 +142,7 @@ export class ImageService {
crop,
} = options;

let transformer = sharp(imageBuffer);
let transformer = sharp(processedImageBuffer);

if (crop && crop.width && crop.height) {
transformer = transformer.extract({
Expand Down
63 changes: 63 additions & 0 deletions packages/api/tests/image.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect } from "bun:test";
import { ImageService } from "../src/services/image.service";

describe("ImageService", () => {
const imageService = new ImageService();

describe("isHeic", () => {
const validSignatures = [
"ftypheic",
"ftypheix",
"ftyphevc",
"ftyphevx",
"ftypmif1",
"ftypmsf1",
];

for (const signature of validSignatures) {
it(`should return true for a HEIC image buffer with signature ${signature}`, () => {
const heicBuffer = Buffer.concat([
Buffer.alloc(4),
Buffer.from(signature),
Buffer.alloc(4),
]);
// @ts-ignore - accessing private method for testing
const result = imageService.isHeic(heicBuffer);
expect(result).toBe(true);
});
}

const validShortSignatures = ["heic", "heix", "hevc", "hevx"];

for (const signature of validShortSignatures) {
it(`should return true for a HEIC image buffer with short signature ${signature}`, () => {
const heicBuffer = Buffer.concat([
Buffer.alloc(8),
Buffer.from(signature),
Buffer.alloc(4),
]);
// @ts-ignore - accessing private method for testing
const result = imageService.isHeic(heicBuffer);
expect(result).toBe(true);
});
}

it("should return false for a non-HEIC image buffer", () => {
const jpegBuffer = Buffer.concat([
Buffer.alloc(4),
Buffer.from("ftypjpeg"),
Buffer.alloc(4),
]);
// @ts-ignore - accessing private method for testing
const result = imageService.isHeic(jpegBuffer);
expect(result).toBe(false);
});

it("should return false for a buffer that is too short", () => {
const shortBuffer = Buffer.from("ftypheic");
// @ts-ignore - accessing private method for testing
const result = imageService.isHeic(shortBuffer);
expect(result).toBe(false);
});
});
});
12 changes: 6 additions & 6 deletions packages/sdk/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("ResizeIt SDK", () => {
});

expect(url).toBe(
"https://api.example.com/resize/images/test.jpg?width=300&height=200&format=webp&quality=80"
"https://api.example.com/images/resize/images/test.jpg?width=300&height=200&format=webp&quality=80"
);
});

Expand All @@ -57,7 +57,7 @@ describe("ResizeIt SDK", () => {
});

expect(url).toBe(
"https://api.example.com/resize/images/test.jpg?width=300&height=200&rotate=90&flip=true&grayscale=true"
"https://api.example.com/images/resize/images/test.jpg?width=300&height=200&rotate=90&flip=true&grayscale=true"
);
});

Expand All @@ -75,7 +75,7 @@ describe("ResizeIt SDK", () => {
});

expect(url).toBe(
"https://api.example.com/resize/images/test.jpg?width=300&height=200&cropLeft=10&cropTop=20&cropWidth=100&cropHeight=100"
"https://api.example.com/images/resize/images/test.jpg?width=300&height=200&cropLeft=10&cropTop=20&cropWidth=100&cropHeight=100"
);
});
});
Expand Down Expand Up @@ -121,7 +121,7 @@ describe("ResizeIt SDK", () => {

// Verify fetch was called with the right arguments
expect(global.fetch).toHaveBeenCalledWith(
"https://api.example.com/upload",
"https://api.example.com/images/upload",
expect.objectContaining({
method: "POST",
headers: {
Expand Down Expand Up @@ -176,7 +176,7 @@ describe("ResizeIt SDK", () => {

// Verify fetch was called with the right arguments including watermark
expect(global.fetch).toHaveBeenCalledWith(
"https://api.example.com/upload",
"https://api.example.com/images/upload",
expect.objectContaining({
method: "POST",
body: expect.stringContaining(
Expand Down Expand Up @@ -257,7 +257,7 @@ describe("ResizeIt SDK", () => {

// Verify fetch was called with the right URL
expect(global.fetch).toHaveBeenCalledWith(
"https://api.example.com/resize/images/test.jpg?width=300&height=200",
"https://api.example.com/images/resize/images/test.jpg?width=300&height=200",
expect.objectContaining({
method: "GET",
})
Expand Down