Next.js Discord

Discord Forum

How to resize image with sharp before uploading it to aws s3

Answered
Yellow croaker posted this in #help-forum
Open in Discord
Yellow croakerOP
Hey everyone, I’m working on image uploads in my app and need to resize images with sharp. Here’s my current setup:

ImageUploader.tsx:

const handleSave = async () => {
  try {
    setIsSaving(true);
    if (imageSrc) {
      const croppedImage = await getCroppedImg(imageSrc, croppedAreaPixels);
      if (!croppedImage) return;
      const checksum = await computeSHA256(croppedImage);
      const signedURLResult = await getSignedURL(croppedImage.type, croppedImage.size, checksum);

      if (signedURLResult.failure) {
        console.error('No Signed Url');
        return;
      }

      const { url } = signedURLResult.success;
      await fetch(url, {
        method: 'PUT',
        body: croppedImage,
        headers: { 'Content-Type': 'image/jpeg' },
      });

      await saveImageToDb(url, resumeId);
    }
  } catch (e) {
    console.error(e);
  } finally {
    setIsSaving(false);
    onClose();
  }
};

return (
  <Button onClick={handleSave}>
    Save Image
  </Button>
);

actions.ts
'use server'
const s3 = new S3Client({...});
const acceptedTypes = ['image/jpeg', 'image/png'];
const maxFileSize = 10 * 1024 * 1024;

export async function getSignedURL(type: string, size: number, checksum: string) {
  const session = await getCurrentUser();
  if (!session) return { failure: 'not authenticated' };
  if (!acceptedTypes.includes(type)) return { failure: 'invalid type' };
  if (size > maxFileSize) return { failure: 'too large' };

  const imageName = generateFileName();
  const putObjectCommand = new PutObjectCommand({
    Bucket: process.env.AWS_BUCKET_NAME,
    Key: imageName,
    ContentType: type,
    ContentLength: size,
    ChecksumSHA256: checksum,
    Metadata: { userId: session.id },
  });

  const signedURL = await getSignedUrl(s3, putObjectCommand, { expiresIn: 60 });
  return { success: { url: signedURL, imageName } };
}


I've tried adding Sharp to the getSignedURL function but couldn't get it to work. How should I implement it?
Answered by joulev
sharp doesn't work in the browser, so you can't resize images with sharp in the browser. so assuming you don't use something else to resize images, you cannot have the cropped image value in the browser. [1]

that means you have to upload the image to a server (could be the nextjs server, could be a third party image resizer, etc), then resize the image there, where sharp is supported. then you simply need to upload from that server to s3 directly, no need to send the image back to the browser anymore. so presigned urls are no longer necessary, you can just S3.send(new PutObjectCommand(...))

[1] if you use a server action or something similar to resize the image, then the same thing holds: you don't need to return the cropped image back to the browser, you can just send the image to s3 directly inside that server action
View full answer

15 Replies

Yellow croakerOP
My current code works basically, but not with sharp, so having 2mb images in s3 is not desirable, when i could easily resize them alot smaller, becuase user really does not need them in full quality.
@Yellow croaker Hey everyone, I’m working on image uploads in my app and need to resize images with sharp. Here’s my current setup: ImageUploader.tsx: ts const handleSave = async () => { try { setIsSaving(true); if (imageSrc) { const croppedImage = await getCroppedImg(imageSrc, croppedAreaPixels); if (!croppedImage) return; const checksum = await computeSHA256(croppedImage); const signedURLResult = await getSignedURL(croppedImage.type, croppedImage.size, checksum); if (signedURLResult.failure) { console.error('No Signed Url'); return; } const { url } = signedURLResult.success; await fetch(url, { method: 'PUT', body: croppedImage, headers: { 'Content-Type': 'image/jpeg' }, }); await saveImageToDb(url, resumeId); } } catch (e) { console.error(e); } finally { setIsSaving(false); onClose(); } }; return ( <Button onClick={handleSave}> Save Image </Button> ); actions.ts ts 'use server' const s3 = new S3Client({...}); const acceptedTypes = ['image/jpeg', 'image/png']; const maxFileSize = 10 * 1024 * 1024; export async function getSignedURL(type: string, size: number, checksum: string) { const session = await getCurrentUser(); if (!session) return { failure: 'not authenticated' }; if (!acceptedTypes.includes(type)) return { failure: 'invalid type' }; if (size > maxFileSize) return { failure: 'too large' }; const imageName = generateFileName(); const putObjectCommand = new PutObjectCommand({ Bucket: process.env.AWS_BUCKET_NAME, Key: imageName, ContentType: type, ContentLength: size, ChecksumSHA256: checksum, Metadata: { userId: session.id }, }); const signedURL = await getSignedUrl(s3, putObjectCommand, { expiresIn: 60 }); return { success: { url: signedURL, imageName } }; } I've tried adding Sharp to the getSignedURL function but couldn't get it to work. How should I implement it?
if you use sharp you cant use presigned urls. you need to upload the image to a server, use sharp in that server, then upload the resized image from the server to s3.

sharp doesn't work in the browser so you can't skip the server middleman
@joulev if you use sharp you cant use presigned urls. you need to upload the image to a server, use sharp in that server, then upload the resized image from the server to s3. sharp doesn't work in the browser so you can't skip the server middleman
Yellow croakerOP
So if I understand correctly, I need to first load the image to the server side as it is now. Then, in the getSignedURL function, I could call a function like compressImage with the image as a parameter. This function would use Sharp to compress the image and return the compressed version, which I would then upload to AWS S3.

Or

In the client side i have value CroppedImage, i send that to the server side and then i use sharp to that image to resize it. Then i return it. So i need to get totally rid of getSignedUrl?

I tried to use sharp in server but i get error :
nextjs warning only plain objects can be passed to client components from server components
@Yellow croaker So if I understand correctly, I need to first load the image to the server side as it is now. Then, in the getSignedURL function, I could call a function like compressImage with the image as a parameter. This function would use Sharp to compress the image and return the compressed version, which I would then upload to AWS S3. Or In the client side i have value CroppedImage, i send that to the server side and then i use sharp to that image to resize it. Then i return it. So i need to get totally rid of getSignedUrl? I tried to use sharp in server but i get error : `nextjs warning only plain objects can be passed to client components from server components`
sharp doesn't work in the browser, so you can't resize images with sharp in the browser. so assuming you don't use something else to resize images, you cannot have the cropped image value in the browser. [1]

that means you have to upload the image to a server (could be the nextjs server, could be a third party image resizer, etc), then resize the image there, where sharp is supported. then you simply need to upload from that server to s3 directly, no need to send the image back to the browser anymore. so presigned urls are no longer necessary, you can just S3.send(new PutObjectCommand(...))

[1] if you use a server action or something similar to resize the image, then the same thing holds: you don't need to return the cropped image back to the browser, you can just send the image to s3 directly inside that server action
Answer
@joulev sharp doesn't work in the browser, so you can't resize images with sharp in the browser. so assuming you don't use something else to resize images, you cannot have the cropped image value in the browser. [1] that means you have to upload the image to a server (could be the nextjs server, could be a third party image resizer, etc), then resize the image there, where sharp is supported. then you simply need to upload from that server to s3 directly, no need to send the image back to the browser anymore. so presigned urls are no longer necessary, you can just `S3.send(new PutObjectCommand(...))` [1] if you use a server action or something similar to resize the image, then the same thing holds: you don't need to return the cropped image back to the browser, you can just send the image to s3 directly inside that server action
Yellow croakerOP
Yeah i think i understand now! Although im having issues on the way how i can send the image to the server side.
'use server'
export async function uploadAndResizeImage(base64Image: string) {
  try {
    const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
    const buffer = Buffer.from(base64Data, 'base64');

    const resizedImageBuffer = await sharp(Buffer.from(buffer))
      .resize(800, 600)
      .toBuffer();

    const filename = generateFileName();

    await s3.send(
      new PutObjectCommand({
        Bucket: process.env.S3_BUCKET_NAME,
        Key: filename,
        Body: resizedImageBuffer,
        ContentType: 'image/jpeg',
      }),
    );
    const imageUrl = `https://${process.env.S3_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${filename}`;
    return { imageUrl };
  } catch (error) {
    throw new Error('Failed to process and upload image');
  }
}


  const handleSave = async () => {
    try {
      setIsSaving(true);
      if (imageSrc) {
        const croppedImage = await getCroppedImg(imageSrc, croppedAreaPixels);
        if (!croppedImage) {
          return;
        }
        const reader = new FileReader();
        reader.readAsDataURL(croppedImage);
        reader.onloadend = async () => {
          const base64data = reader.result as string;
          const result = await uploadAndResizeImage(base64data);
          await saveImageToDb(result.imageUrl, resumeId);
        };
      }
    } catch (e) {
      console.error(e);
    } finally {
      setIsSaving(false);
      onClose();
    }
  };


If i try to send it as blob i get
Error: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.

and if i try to send it as base64 i get:
Error: Body exceeded undefined limit.
To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/server-actions#size-limitation
@Yellow croaker Yeah i think i understand now! Although im having issues on the way how i can send the image to the server side. ts 'use server' export async function uploadAndResizeImage(base64Image: string) { try { const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, ''); const buffer = Buffer.from(base64Data, 'base64'); const resizedImageBuffer = await sharp(Buffer.from(buffer)) .resize(800, 600) .toBuffer(); const filename = generateFileName(); await s3.send( new PutObjectCommand({ Bucket: process.env.S3_BUCKET_NAME, Key: filename, Body: resizedImageBuffer, ContentType: 'image/jpeg', }), ); const imageUrl = `https://${process.env.S3_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${filename}`; return { imageUrl }; } catch (error) { throw new Error('Failed to process and upload image'); } } ts const handleSave = async () => { try { setIsSaving(true); if (imageSrc) { const croppedImage = await getCroppedImg(imageSrc, croppedAreaPixels); if (!croppedImage) { return; } const reader = new FileReader(); reader.readAsDataURL(croppedImage); reader.onloadend = async () => { const base64data = reader.result as string; const result = await uploadAndResizeImage(base64data); await saveImageToDb(result.imageUrl, resumeId); }; } } catch (e) { console.error(e); } finally { setIsSaving(false); onClose(); } }; If i try to send it as blob i get `Error: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.` and if i try to send it as base64 i get: Error: Body exceeded undefined limit. To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/server-actions#size-limitation
I think you can use form data to send the file.

const form = new FormData()
form.append("file", file)
uploadAndResizeImage(form)

function uploadAndResizeImage(form) {
  const file = form.get("file") as File
}
@joulev I think you can use form data to send the file. tsx const form = new FormData() form.append("file", file) uploadAndResizeImage(form) tsx function uploadAndResizeImage(form) { const file = form.get("file") as File }
Yellow croakerOP
Yeah this worked nicely thanks!
The end code looks like this:
'use server'
export async function uploadAndResizeImage(formData: FormData) {
  try {
    const file = formData.get('file') as File;
    if (!file) {
      throw new Error('No file uploaded');
    }
    const buffer = await file.arrayBuffer();
    const resizedImageBuffer = await sharp(Buffer.from(buffer))
      .resize(800, 600)
      .toBuffer();

    const filename = generateFileName();

    await s3.send(
      new PutObjectCommand({
        Bucket: process.env.AWS_BUCKET_NAME,
        Key: filename,
        Body: resizedImageBuffer,
        ContentType: 'image/jpeg',
      }),
    );
    const imageUrl = `https://${process.env.AWS_BUCKET_NAME}.s3.${process.env.AWS_BUCKET_REGION}.amazonaws.com/${filename}`;
    return { imageUrl };
  } catch (error) {
    console.error('Error processing and uploading image:', error);
    throw new Error('Failed to process and upload image');
  }
}


'use client'
  const handleSave = async () => {
    try {
      setIsSaving(true);
      if (imageSrc) {
        const croppedImage = await getCroppedImg(imageSrc, croppedAreaPixels);
        if (croppedImage === undefined || croppedImage === null) {
          console.error('Failed to crop image');
          return;
        }
        const form = new FormData();
        const result = await uploadAndResizeImage(form);
        await saveImageToDb(result.imageUrl, resumeId);
      }
    } catch (e) {
      console.error(e);
    } finally {
      setIsSaving(false);
      onClose();
    }
  };

Im calling the saveImageToDb in client side, would it be better to call it in the server action, so we dont actually need to return anything from there and then that saveImageToDbwould call revalidatePath
@Yellow croaker Yeah this worked nicely thanks! The end code looks like this: ts 'use server' export async function uploadAndResizeImage(formData: FormData) { try { const file = formData.get('file') as File; if (!file) { throw new Error('No file uploaded'); } const buffer = await file.arrayBuffer(); const resizedImageBuffer = await sharp(Buffer.from(buffer)) .resize(800, 600) .toBuffer(); const filename = generateFileName(); await s3.send( new PutObjectCommand({ Bucket: process.env.AWS_BUCKET_NAME, Key: filename, Body: resizedImageBuffer, ContentType: 'image/jpeg', }), ); const imageUrl = `https://${process.env.AWS_BUCKET_NAME}.s3.${process.env.AWS_BUCKET_REGION}.amazonaws.com/${filename}`; return { imageUrl }; } catch (error) { console.error('Error processing and uploading image:', error); throw new Error('Failed to process and upload image'); } } ts 'use client' const handleSave = async () => { try { setIsSaving(true); if (imageSrc) { const croppedImage = await getCroppedImg(imageSrc, croppedAreaPixels); if (croppedImage === undefined || croppedImage === null) { console.error('Failed to crop image'); return; } const form = new FormData(); const result = await uploadAndResizeImage(form); await saveImageToDb(result.imageUrl, resumeId); } } catch (e) { console.error(e); } finally { setIsSaving(false); onClose(); } }; Im calling the saveImageToDb in client side, would it be better to call it in the server action, so we dont actually need to return anything from there and then that `saveImageToDb`would call revalidatePath
i would call it directly in the server action yeah, no point doing another network round trip
keep in mind though that since this involves uploading files to the server, certain server limits apply. for example on vercel your file can't be bigger than 4.5MB (this is a hard limit that cannot be overcome). if you self-host you need to put a limit as well to prevent people from loading a huge file onto the server RAM.
for images most of them should work fine, i use a 2MB limit in my apps
@joulev keep in mind though that since this involves uploading files to the server, certain server limits apply. for example on vercel your file can't be bigger than 4.5MB (this is a hard limit that cannot be overcome). if you self-host you need to put a limit as well to prevent people from loading a huge file onto the server RAM.
Yellow croakerOP
Yeah i'm using coolify with hertzner vps.

Should the limit be on client or server side? Probably on server side. So basically i just add something like this to the server action:

const maxFileSize = 1024 * 1024 * 10; // 10MB Decrease this size

export async function uploadAndResizeImage(formData: FormData) {
  const session = await getCurrentUser();
  console.log('Session in resumeACtion', session);
  if (!session) {
    return { failure: 'not authencticated' };
  }

  if (!acceptedTypes.includes(type)) {
    return { failure: 'invalid type' };
  }
// get size value from somewhere :)
  if (size > maxFileSize) {
    return { failure: 'file too large' };
  }
...
@joulev like when the user uploads a 1GB image, you can instantly tell them this file is too big without attempting to upload the image to your server
Yellow croakerOP
Yeah make sense! Thanks from your help!
Cant find button to mark this "answered" and the help forum rules and guidelines does not have guide to do that.
you're welcome