skip to Main Content

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.

2

Answers


  1. Usually IMG elements have an onload or load event handler on the element to handle image file’s content after it’s been read. Possibly the first attempt to compress the image fails because the image hasn’t been read, and the second attempt succeeds because of some kind of synchronous processing of image data held in a memory cache. But that’s a big maybe.

    • Provide a load event handler on the image element, and wait for it to fire before writing the image to a canvas.

    • Provide an ‘error` event handler on the image element to handle error conditions.

    Login or Signup to reply.
  2. 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; 
       });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search