feat: image compression w/ sharp
This commit is contained in:
parent
7b11266f72
commit
861bb01a31
7 changed files with 1626 additions and 52 deletions
1600
website/package-lock.json
generated
1600
website/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -58,6 +58,7 @@
|
||||||
"openai": "^4.103.0",
|
"openai": "^4.103.0",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
"redis": "^5.1.0",
|
"redis": "^5.1.0",
|
||||||
|
"sharp": "^0.34.2",
|
||||||
"svelte-apexcharts": "^1.0.2",
|
"svelte-apexcharts": "^1.0.2",
|
||||||
"svelte-confetti": "^2.3.1",
|
"svelte-confetti": "^2.3.1",
|
||||||
"svelte-lightweight-charts": "^2.2.0"
|
"svelte-lightweight-charts": "^2.2.0"
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,7 @@ export const auth = betterAuth({
|
||||||
s3ImageKey = await uploadProfilePicture(
|
s3ImageKey = await uploadProfilePicture(
|
||||||
profile.sub,
|
profile.sub,
|
||||||
new Uint8Array(arrayBuffer),
|
new Uint8Array(arrayBuffer),
|
||||||
blob.type,
|
blob.type || 'image/jpeg'
|
||||||
blob.size
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
39
website/src/lib/server/image.ts
Normal file
39
website/src/lib/server/image.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
const MAX_SIZE = 128;
|
||||||
|
const WEBP_QUALITY = 50;
|
||||||
|
|
||||||
|
export interface ProcessedImage {
|
||||||
|
buffer: Buffer;
|
||||||
|
contentType: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processImage(
|
||||||
|
inputBuffer: Buffer,
|
||||||
|
): Promise<ProcessedImage> {
|
||||||
|
try {
|
||||||
|
const image = sharp(inputBuffer, { animated: true });
|
||||||
|
|
||||||
|
const processedBuffer = await image
|
||||||
|
.resize(MAX_SIZE, MAX_SIZE, {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true
|
||||||
|
})
|
||||||
|
.webp({
|
||||||
|
quality: WEBP_QUALITY,
|
||||||
|
effort: 6
|
||||||
|
})
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: processedBuffer,
|
||||||
|
contentType: 'image/webp',
|
||||||
|
size: processedBuffer.length
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Image processing failed:', error);
|
||||||
|
throw new Error('Failed to process image');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } fro
|
||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||||
import { PRIVATE_B2_KEY_ID, PRIVATE_B2_APP_KEY } from '$env/static/private';
|
import { PRIVATE_B2_KEY_ID, PRIVATE_B2_APP_KEY } from '$env/static/private';
|
||||||
import { PUBLIC_B2_BUCKET, PUBLIC_B2_ENDPOINT, PUBLIC_B2_REGION } from '$env/static/public';
|
import { PUBLIC_B2_BUCKET, PUBLIC_B2_ENDPOINT, PUBLIC_B2_REGION } from '$env/static/public';
|
||||||
|
import { processImage } from './image.js';
|
||||||
|
|
||||||
const s3Client = new S3Client({
|
const s3Client = new S3Client({
|
||||||
endpoint: PUBLIC_B2_ENDPOINT,
|
endpoint: PUBLIC_B2_ENDPOINT,
|
||||||
|
|
@ -47,7 +48,6 @@ export async function uploadProfilePicture(
|
||||||
identifier: string, // Can be user ID or a unique ID from social provider
|
identifier: string, // Can be user ID or a unique ID from social provider
|
||||||
body: Uint8Array,
|
body: Uint8Array,
|
||||||
contentType: string,
|
contentType: string,
|
||||||
contentLength?: number
|
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!contentType || !contentType.startsWith('image/')) {
|
if (!contentType || !contentType.startsWith('image/')) {
|
||||||
throw new Error('Invalid file type. Only images are allowed.');
|
throw new Error('Invalid file type. Only images are allowed.');
|
||||||
|
|
@ -58,17 +58,16 @@ export async function uploadProfilePicture(
|
||||||
throw new Error('Unsupported image format. Only JPEG, PNG, GIF, and WebP are allowed.');
|
throw new Error('Unsupported image format. Only JPEG, PNG, GIF, and WebP are allowed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileExtension = contentType.split('/')[1];
|
const processedImage = await processImage(Buffer.from(body));
|
||||||
if (fileExtension === 'jpeg') fileExtension = 'jpg';
|
|
||||||
|
|
||||||
const key = `avatars/${identifier}.${fileExtension}`;
|
const key = `avatars/${identifier}.webp`;
|
||||||
|
|
||||||
const command = new PutObjectCommand({
|
const command = new PutObjectCommand({
|
||||||
Bucket: PUBLIC_B2_BUCKET,
|
Bucket: PUBLIC_B2_BUCKET,
|
||||||
Key: key,
|
Key: key,
|
||||||
Body: body,
|
Body: processedImage.buffer,
|
||||||
ContentType: contentType,
|
ContentType: processedImage.contentType,
|
||||||
...(contentLength && { ContentLength: contentLength }),
|
ContentLength: processedImage.size,
|
||||||
});
|
});
|
||||||
|
|
||||||
await s3Client.send(command);
|
await s3Client.send(command);
|
||||||
|
|
@ -79,7 +78,6 @@ export async function uploadCoinIcon(
|
||||||
coinSymbol: string,
|
coinSymbol: string,
|
||||||
body: Uint8Array,
|
body: Uint8Array,
|
||||||
contentType: string,
|
contentType: string,
|
||||||
contentLength?: number
|
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!contentType || !contentType.startsWith('image/')) {
|
if (!contentType || !contentType.startsWith('image/')) {
|
||||||
throw new Error('Invalid file type. Only images are allowed.');
|
throw new Error('Invalid file type. Only images are allowed.');
|
||||||
|
|
@ -90,17 +88,16 @@ export async function uploadCoinIcon(
|
||||||
throw new Error('Unsupported image format. Only JPEG, PNG, GIF, and WebP are allowed.');
|
throw new Error('Unsupported image format. Only JPEG, PNG, GIF, and WebP are allowed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileExtension = contentType.split('/')[1];
|
const processedImage = await processImage(Buffer.from(body));
|
||||||
if (fileExtension === 'jpeg') fileExtension = 'jpg';
|
|
||||||
|
|
||||||
const key = `coins/${coinSymbol.toLowerCase()}.${fileExtension}`;
|
const key = `coins/${coinSymbol.toLowerCase()}.webp`;
|
||||||
|
|
||||||
const command = new PutObjectCommand({
|
const command = new PutObjectCommand({
|
||||||
Bucket: PUBLIC_B2_BUCKET,
|
Bucket: PUBLIC_B2_BUCKET,
|
||||||
Key: key,
|
Key: key,
|
||||||
Body: body,
|
Body: processedImage.buffer,
|
||||||
ContentType: contentType,
|
ContentType: processedImage.contentType,
|
||||||
...(contentLength && { ContentLength: contentLength }),
|
ContentLength: processedImage.size,
|
||||||
});
|
});
|
||||||
|
|
||||||
await s3Client.send(command);
|
await s3Client.send(command);
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,7 @@ async function handleIconUpload(iconFile: File | null, symbol: string): Promise<
|
||||||
return await uploadCoinIcon(
|
return await uploadCoinIcon(
|
||||||
symbol,
|
symbol,
|
||||||
new Uint8Array(arrayBuffer),
|
new Uint8Array(arrayBuffer),
|
||||||
iconFile.type,
|
iconFile.type
|
||||||
iconFile.size
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,7 @@ export async function POST({ request }) {
|
||||||
const key = await uploadProfilePicture(
|
const key = await uploadProfilePicture(
|
||||||
session.user.id,
|
session.user.id,
|
||||||
new Uint8Array(arrayBuffer),
|
new Uint8Array(arrayBuffer),
|
||||||
avatarFile.type,
|
avatarFile.type
|
||||||
avatarFile.size
|
|
||||||
);
|
);
|
||||||
updates.image = key;
|
updates.image = key;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
Reference in a new issue