I’ve built a Slack-style avatar image upload and crop feature, and I can’t get the cropped image to save without corruption. The original file upload, using the same endpoint and method, works just fine. It’s the cropped copy, created manually from a Blob, that is always corrupt.
Steps are pretty simple:
- Select and upload an image file
- Crop (react-image-crop) appears
- Select area, hit Save
In Step 1, the file is uploaded when the file input changes. File is sent to a streaming endpoint which uploads the file to an s3 bucket.
<button type="button">Upload</button>
<input
type="file"
accept="image/*"
onChange={onFileChange}
/>
...
const onFileChange = async e => {
e.preventDefault();
let uploadedFile = e.target.files[0];
await onSave(uploadedFile);
};
The uploadedFile var is the File object returned from the input control. This works great! No issues, yet.
In Step 3, once you’ve selected an area of the image, a Blob is produced by react-image-crop.
const getCroppedImage = (source, config) => {
const canvas = document.createElement("canvas");
const scaleX = source.naturalWidth / source.width;
const scaleY = source.naturalHeight / source.height;
canvas.width = config.width;
canvas.height = config.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(
source,
config.x * scaleX,
config.y * scaleY,
config.width * scaleX,
config.height * scaleY,
0,
0,
config.width,
config.height
);
let mimeType = mime.lookup(userProfile.image_file.split(".").at(-1));
return new Promise((resolve, reject) => {
canvas.toBlob(blob => {
if (!blob) {
reject(new Error("Canvas is empty"));
return;
}
resolve(blob); //***THIS BLOB...
}, mimeType);
});
};
This Blob is valid, because I display the selected area on the screen before saving:
const AvatarPreview = () => {
if (activeAvatar) {
return <ImageCropper imageToCrop={activeAvatar} onImageCropped={onImageCropped} />;
}
return <Icon icon="bi:person" />;
};
I stuff the Blob produced by react-image-crop, into a File object because that’s what my code expects, just like Step 1.
const onImageCropped = croppedBlob => { //***IS PASSED IN HERE
let croppedImg = URL.createObjectURL(croppedBlob);
setActiveAvatar(croppedImg);
const reader = new FileReader();
reader.readAsDataURL(croppedBlob);
reader.addEventListener("load", () => {
let { result } = reader;
let resultMimeType = result.split(";")[0].split(":")[1];
let croppedFile = new File([result], userProfile.image_file, { type: resultMimeType }); //***NEW File object, from Blob
setCroppedAvatar(croppedFile);
}, false);
};
<button type="button" onClick={() => onSave(croppedAvatar)}>Save</button>
The “result” in the FileReader load above is the base64 image data:
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA…QCMFWeFZ9u3fPsWY3hh9uVHWllbSwtGASrIOQc1t6rt0OBqG6x7aVfdY859pnIwfDDhCfpwBUyaSus4VZO+susGGM60zrB1TuEjx5Ov76FBKUgNTltIQlP4UpQghKQMAAAADHApt6lv1x1HeHrzdny9KkJbDrpHLhQhKNx91EJBJ9SSaQkLS+4HTwvalKvmQMZ/PFa8hBCRntnirYUDqVYtEWlQ8JYJSrv8vnTy6W2h5/qBYmCgqbcmtgLSMg+YUyYD4aUUqTlJ4I+td+FLethRhai0sgoUOCg+hFPBwcxjruUie1k/VDOmenKkIcS2iJCzyeAAmvF7qjqFzVWurzfHV7jJluKBJ/hBwP6VL1u+KvqlD0lL0VeLt+2rZLjmO25MyqSwNuPK7nKh8l7vlioFejqU8tx453EnIPcmrWrvW5VFf6zK0GifTWs1n6TRoUrIaDZBSeDSVUzyMTY7gBIORR/EcXwTnFEozfc0xVwOYT/2Q=='
The new File object seems legit to me:
I then send the image through the endpoint again, and it’s uploaded. It appears in the bucket, and the file size seems legit (not 0KB like a broken stream would indicate.)
However, upon downloading and attempting to open the file, it’s corrupt. I guess I’m missing an option somewhere…some little tweak that would make this work? Is the File object not formed correctly? How do I troubleshoot this further?