I'm creating a screenshot submission form, and I want to compress the image before uploading it to my server. I've got the process so it works, but whenever I test it the first time I try submitting it fails. Clicking the submit button a second time, without changing anything, without re-selecting the file, just simply clicking submit again, it succeeds as I would expect it to.
I'm using Next.js, with swr and react-toastify to handle data fetching/caching and user notification respectively.
This is my compression function.
const compressImage = imgData =>
new Promise((resolve, reject) => {
const img = document.createElement("img");
img.src = imgData;
console.log(img);
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const originalWidth = img.width;
const originalHeight = img.height;
console.log(originalWidth, originalHeight);
const baseWidth = 480;
const baseHeight = 270;
const canvasWidth = Math.min(baseWidth, ((originalWidth * baseHeight) / originalHeight) | 0);
const canvasHeight = Math.min(baseHeight, ((originalHeight * baseWidth) / originalWidth) | 0);
console.log(canvasWidth, canvasHeight);
canvas.width = Math.min(originalWidth, canvasWidth);
canvas.height = Math.min(originalHeight, canvasHeight);
console.log(canvas.width, canvas.height);
try {
context.drawImage(img, 0, 0, canvasWidth, canvasHeight);
} catch (err) {
return reject(err);
}
// Reduce quality
canvas.toBlob(blob => {
if (blob) resolve(blob);
else reject("No blob");
}, "image/jpeg");
});
I suspect the issue is either here with how I'm creating the img element, or somewhere before here and this is where it's making itself obvious.
When I check the logs I see the first attempt shows me the src being set correctly, but a size of 0 0 which then propagates down, and my expectation is that when the canvas size is 0 there's nothing to put in the blob at the end.
<img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD…">
0 0
0 0
0 0
The second submit gives be logs like I would expect to see.
<img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD…">
1920 1080
480 270
480 270
I'm a bit lost on what to look for or what to change. It feels like somehow there's a react hook that's using stale data, but then why would the img src be correct in the compression function.
I've created this form for submitting:
<Modal.Body>
{screenshot && (
<img
src={screenshot}
alt="Submitted Screenshot"
width="100%"
style={{ objectFit: "contain" }}
/>
)}
<form
id="screenshotForm"
action={async formData => {
setSubmitting(true);
const screenshotFile = formData.get("screenshot");
// Is this actually redundant?
const screenshotData = await new Promise(resolve => {
const fr = new FileReader();
fr.addEventListener("load", e => resolve(e.target.result));
fr.readAsDataURL(screenshotFile);
});
try {
const compressedData = await compressImage(screenshotData);
const blobUrl = URL.createObjectURL(compressedData);
setScreenshot(blobUrl);
const formData = new FormData();
formData.append("image", compressedData);
formData.append("beatmapId", selectedMap.id);
formData.append("mods", selectedMap.mods);
await toast.promise(
mutate("/api/db/player", () => uploadScreenshot(formData), {
optimisticData: oldData => {
const updatedMaplist = oldData.maps.current;
const index = updatedMaplist.findIndex(
m => m.id === selectedMap.id && m.mods === selectedMap.mods
);
updatedMaplist[index].screenshot = compressedData.arrayBuffer();
return {
...oldData,
maps: {
...oldData.maps,
current: updatedMaplist
}
};
},
populateCache: (result, oldData) => ({
...oldData,
maps: {
...oldData.maps,
current: result
}
})
}),
{
pending: "Uploading",
success: "Image uploaded",
error: "Unable to upload image"
}
);
} catch (err) {
toast.error("Unable to upload image");
console.error(err);
}
setSubmitting(false);
}}
>
<FormLabel htmlFor="imageUpload">Upload Screenshot</FormLabel>
<FormControl type="file" accept="image/*" id="imageUpload" name="screenshot" />
</form>
</Modal.Body>
<Modal.Footer>
<Button type="submit" form="screenshotForm" disabled={submitting}>
Submit {submitting && <Spinner size="sm" />}
</Button>
<Button onClick={() => setShowModal(false)}>Done</Button>
</Modal.Footer>
Both of these are in the page.js file, which is declared as "use client";
uploadScreenshot is imported from './actions' which is "use server";
Another possible symptom is that the spinner on the button shown by submitting
is never visible. But if that's a separate issue then I don't care about it here.
The issue is related to asynchronous operations. The image element created in the compressImage function does not have time to load before you attempt to draw it on the canvas means image is not loaded yet and you move forward and attempt to draw it on canvas that's why seeing dimensions 0x0.
SOLUTION: You should have to add onload event handler to image element so it ensures that the image is loaded before trying to draw it on canvas
CODE:
const compressImage = imgData =>
new Promise((resolve, reject) => {
const img = document.createElement("img");
// Set up the onload event
img.onload = () => {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const originalWidth = img.width;
const originalHeight = img.height;
const baseWidth = 480;
const baseHeight = 270;
const canvasWidth = Math.min(baseWidth, ((originalWidth * baseHeight) / originalHeight) | 0);
const canvasHeight = Math.min(baseHeight, ((originalHeight * baseWidth) / originalWidth) | 0);
canvas.width = Math.min(originalWidth, canvasWidth);
canvas.height = Math.min(originalHeight, canvasHeight);
context.drawImage(img, 0, 0, canvasWidth, canvasHeight);
// Reduce quality
canvas.toBlob(blob => {
if (blob) resolve(blob);
else reject("No blob");
}, "image/jpeg");
};
// Handle errors
img.onerror = () => {
reject("Image loading error");
};
img.src = imgData;
});