Skip to content

Instantly share code, notes, and snippets.

@simonw
Last active April 8, 2023 20:21
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save simonw/66918b6cde1f87bf4fc883c67735195d to your computer and use it in GitHub Desktop.
Save simonw/66918b6cde1f87bf4fc883c67735195d to your computer and use it in GitHub Desktop.

Drag-Drop Image Conversion

2023-04-01 19:26:03

user: HTML and JavaScript for a page where a user can drag and drop an image onto a div and that image will then be turned into a 500px wide JPEG displayed below on the page, plus a textarea containing a <img src= data URI> image that can be copied and pasted out.


assistant: To create an HTML page where a user can drag and drop an image onto a div, and then have that image converted to a 500px wide JPEG displayed below on the page, along with a textarea containing an <img src= data URI> image that can be copied and pasted out, you can use the following HTML and JavaScript code:

<!DOCTYPE html>
<html>
<head>
  <title>Drag and Drop Image</title>
  <style>
    #dropzone {
      width: 500px;
      height: 300px;
      border: 2px dashed #ccc;
      text-align: center;
      padding: 20px;
      margin-bottom: 20px;
    }
    #output {
      width: 500px;
      margin-bottom: 20px;
    }
    textarea {
      width: 500px;
      height: 100px;
    }
  </style>
</head>
<body>
  <div id="dropzone">Drag and drop an image here</div>
  <img id="output" src="" alt="Converted Image" />
  <textarea id="imgDataUri" readonly><textarea>

  <script>
    const dropzone = document.getElementById('dropzone');
    const output = document.getElementById('output');
    const imgDataUri = document.getElementById('imgDataUri');

    dropzone.addEventListener('dragover', (e) => {
      e.preventDefault();
      dropzone.style.backgroundColor = '#f0f0f0';
    });

    dropzone.addEventListener('dragleave', () => {
      dropzone.style.backgroundColor = '';
    });

    dropzone.addEventListener('drop', (e) => {
      e.preventDefault();
      dropzone.style.backgroundColor = '';

      const file = e.dataTransfer.files[0];
      if (file && file.type.startsWith('image/')) {
        const reader = new FileReader();
        reader.onload = (e) => {
          const img = new Image();
          img.onload = () => {
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            const aspectRatio = img.height / img.width;
            canvas.width = 500;
            canvas.height = 500 * aspectRatio;
            ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
            const dataUrl = canvas.toDataURL('image/jpeg');
            output.src = dataUrl;
            imgDataUri.value = `<img src="${dataUrl}" alt="Converted Image" />`;
          };
          img.src = e.target.result;
        };
        reader.readAsDataURL(file);
      } else {
        alert('Please drop an image file.');
      }
    });
  </script>
</body>
</html>

In this code, we define a div element with the ID dropzone that serves as the drop zone for the image. We also define an img element with the ID output to display the converted image and a textarea element with the ID imgDataUri to display the <img src= data URI> code.

The JavaScript code listens for the dragover, dragleave, and drop events on the dropzone element. When an image is dropped onto the dropzone, the code reads the image file, resizes it to 500px wide while maintaining the aspect ratio, converts it to a JPEG format, and displays it in the output image element. The data URI of the image is also displayed in the imgDataUri textarea, ready to be copied and pasted out.


user: Add a slider to control the JPEG quality from 0 to 100 - as the slider is moved the JPEG and the textarea should both update


assistant: To add a slider that controls the JPEG quality and updates the JPEG and the textarea as the slider is moved, you can modify the existing code as follows:

<!DOCTYPE html>
<html>
<head>
  <title>Drag and Drop Image</title>
  <style>
    #dropzone {
      width: 500px;
      height: 300px;
      border: 2px dashed #ccc;
      text-align: center;
      padding: 20px;
      margin-bottom: 20px;
    }
    #output {
      width: 500px;
      margin-bottom: 20px;
    }
    textarea {
      width: 500px;
      height: 100px;
    }
  </style>
</head>
<body>
  <div id="dropzone">Drag and drop an image here</div>
  <label for="qualityRange">JPEG Quality: <span id="qualityValue">75</span></label>
  <input type="range" id="qualityRange" min="0" max="100" value="75" />
  <img id="output" src="" alt="Converted Image" />
  <textarea id="imgDataUri" readonly><textarea>

  <script>
    const dropzone = document.getElementById('dropzone');
    const output = document.getElementById('output');
    const imgDataUri = document.getElementById('imgDataUri');
    const qualityRange = document.getElementById('qualityRange');
    const qualityValue = document.getElementById('qualityValue');
    let imgSrc;

    dropzone.addEventListener('dragover', (e) => {
      e.preventDefault();
      dropzone.style.backgroundColor = '#f0f0f0';
    });

    dropzone.addEventListener('dragleave', () => {
      dropzone.style.backgroundColor = '';
    });

    dropzone.addEventListener('drop', (e) => {
      e.preventDefault();
      dropzone.style.backgroundColor = '';

      const file = e.dataTransfer.files[0];
      if (file && file.type.startsWith('image/')) {
        const reader = new FileReader();
        reader.onload = (e) => {
          imgSrc = e.target.result;
          updateImage();
        };
        reader.readAsDataURL(file);
      } else {
        alert('Please drop an image file.');
      }
    });

    qualityRange.addEventListener('input', () => {
      qualityValue.textContent = qualityRange.value;
      updateImage();
    });

    function updateImage() {
      if (!imgSrc) return;
      const img = new Image();
      img.onload = () => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        const aspectRatio = img.height / img.width;
        canvas.width = 500;
        canvas.height = 500 * aspectRatio;
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        const quality = qualityRange.value / 100;
        const dataUrl = canvas.toDataURL('image/jpeg', quality);
        output.src = dataUrl;
        imgDataUri.value = `<img src="${dataUrl}" alt="Converted Image" />`;
      };
      img.src = imgSrc;
    }
  </script>
</body>
</html>

In this modified code, we added a label and an input element of type range with the ID qualityRange to create the slider for controlling the JPEG quality. The qualityValue span element displays the current value of the slider.

We also added an event listener for the input event on the qualityRange slider. When the slider value changes, the updateImage function is called to update the displayed image and the textarea content.

The updateImage function reads the


assistant: current value of the slider, converts it to a quality factor between 0 and 1, and uses it to generate a new JPEG image with the specified quality. The toDataURL method accepts the second argument as the quality factor, which determines the quality of the generated JPEG image. The updated image is then displayed in the output image element, and the data URI of the image is displayed in the imgDataUri textarea.

The imgSrc variable is used to store the source of the original image, so we can regenerate the JPEG image with different quality settings as the slider is moved. The updateImage function is also called when a new image is dropped onto the dropzone, ensuring that the image is displayed with the current quality setting from the slider.


user: Update the design so each element is on a newline, and add a div that shows the size of the JPEG in bytes


assistant: To update the design so that each element is on a new line, and to add a div that shows the size of the JPEG in bytes, you can modify the existing code as follows:

<!DOCTYPE html>
<html>
<head>
  <title>Drag and Drop Image</title>
  <style>
    #dropzone {
      width: 500px;
      height: 300px;
      border: 2px dashed #ccc;
      text-align: center;
      padding: 20px;
      margin-bottom: 20px;
    }
    #output {
      width: 500px;
      margin-bottom: 20px;
    }
    textarea {
      width: 500px;
      height: 100px;
    }
    .container {
      margin-bottom: 20px;
    }
  </style>
</head>
<body>
  <div id="dropzone">Drag and drop an image here</div>
  <div class="container">
    <label for="qualityRange">JPEG Quality: <span id="qualityValue">75</span></label><br>
    <input type="range" id="qualityRange" min="0" max="100" value="75" />
  </div>
  <img id="output" src="" alt="Converted Image" /><br>
  <div class="container">
    <label for="imgDataUri">Image Data URI:</label><br>
    <textarea id="imgDataUri" readonly><textarea>
  </div>
  <div id="jpegSize">JPEG Size: 0 bytes</div>

  <script>
    const dropzone = document.getElementById('dropzone');
    const output = document.getElementById('output');
    const imgDataUri = document.getElementById('imgDataUri');
    const qualityRange = document.getElementById('qualityRange');
    const qualityValue = document.getElementById('qualityValue');
    const jpegSize = document.getElementById('jpegSize');
    let imgSrc;

    dropzone.addEventListener('dragover', (e) => {
      e.preventDefault();
      dropzone.style.backgroundColor = '#f0f0f0';
    });

    dropzone.addEventListener('dragleave', () => {
      dropzone.style.backgroundColor = '';
    });

    dropzone.addEventListener('drop', (e) => {
      e.preventDefault();
      dropzone.style.backgroundColor = '';

      const file = e.dataTransfer.files[0];
      if (file && file.type.startsWith('image/')) {
        const reader = new FileReader();
        reader.onload = (e) => {
          imgSrc = e.target.result;
          updateImage();
        };
        reader.readAsDataURL(file);
      } else {
        alert('Please drop an image file.');
      }
    });

    qualityRange.addEventListener('input', () => {
      qualityValue.textContent = qualityRange.value;
      updateImage();
    });

    function updateImage() {
      if (!imgSrc) return;
      const img = new Image();
      img.onload = () => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        const aspectRatio = img.height / img.width;
        canvas.width = 500;
        canvas.height = 500 * aspectRatio;
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        const quality = qualityRange.value / 100;
        const dataUrl = canvas.toDataURL('image/jpeg', quality);
        output.src = dataUrl;
        imgDataUri.value = `<img src="${dataUrl}" alt="Converted Image" />`;
        const size = Math.round(dataUrl.length * 3 / 4); // Approximate size in bytes
        jpegSize.textContent = `JPEG Size: ${size}

---
**assistant**:  bytes`;
      };
      img.src = imgSrc;
    }
  </script>
</body>
</html>

In this modified code, we added <br> tags after the slider and the image to ensure that each element is on a new line. We also added a new div element with the ID jpegSize to display the size of the JPEG in bytes.

We introduced a new variable jpegSize to reference the div element that shows the size of the JPEG. In the updateImage function, we calculate the approximate size of the JPEG in bytes based on the length of the data URL. The size is then displayed in the jpegSize div.

Note that the calculated size is an approximation because the data URL is base64 encoded, and the actual size of the binary data is roughly 3/4 of the length of the base64 string. The size is rounded to the nearest whole number for display.

The container class is added to group elements and provide a margin at the bottom for better spacing. The label for the textarea is also added to improve accessibility.


user: Can that output PNG too?


assistant: Yes, the output can be changed to PNG format. To do this, we need to add a new dropdown menu that allows the user to select the desired output format (JPEG or PNG). We also need to modify the updateImage function to generate the image in the selected format. Here's the updated code:

<!DOCTYPE html>
<html>
<head>
  <title>Drag and Drop Image</title>
  <style>
    #dropzone {
      width: 500px;
      height: 300px;
      border: 2px dashed #ccc;
      text-align: center;
      padding: 20px;
      margin-bottom: 20px;
    }
    #output {
      width: 500px;
      margin-bottom: 20px;
    }
    textarea {
      width: 500px;
      height: 100px;
    }
    .container {
      margin-bottom: 20px;
    }
  </style>
</head>
<body>
  <div id="dropzone">Drag and drop an image here</div>
  <div class="container">
    <label for="outputFormat">Output Format:</label>
    <select id="outputFormat">
      <option value="image/jpeg">JPEG</option>
      <option value="image/png">PNG</option>
    </select>
  </div>
  <div class="container">
    <label for="qualityRange">JPEG Quality: <span id="qualityValue">75</span></label><br>
    <input type="range" id="qualityRange" min="0" max="100" value="75" />
  </div>
  <img id="output" src="" alt="Converted Image" /><br>
  <div class="container">
    <label for="imgDataUri">Image Data URI:</label><br>
    <textarea id="imgDataUri" readonly><textarea>
  </div>
  <div id="jpegSize">Image Size: 0 bytes</div>

  <script>
    const dropzone = document.getElementById('dropzone');
    const output = document.getElementById('output');
    const imgDataUri = document.getElementById('imgDataUri');
    const qualityRange = document.getElementById('qualityRange');
    const qualityValue = document.getElementById('qualityValue');
    const outputFormat = document.getElementById('outputFormat');
    const jpegSize = document.getElementById('jpegSize');
    let imgSrc;

    dropzone.addEventListener('dragover', (e) => {
      e.preventDefault();
      dropzone.style.backgroundColor = '#f0f0f0';
    });

    dropzone.addEventListener('dragleave', () => {
      dropzone.style.backgroundColor = '';
    });

    dropzone.addEventListener('drop', (e) => {
      e.preventDefault();
      dropzone.style.backgroundColor = '';

      const file = e.dataTransfer.files[0];
      if (file && file.type.startsWith('image/')) {
        const reader = new FileReader();
        reader.onload = (e) => {
          imgSrc = e.target.result;
          updateImage();
        };
        reader.readAsDataURL(file);
      } else {
        alert('Please drop an image file.');
      }
    });

    qualityRange.addEventListener('input', () => {
      qualityValue.textContent = qualityRange.value;
      updateImage();
    });

    outputFormat.addEventListener('change', () => {
      updateImage();
    });

    function updateImage() {
      if (!imgSrc) return;
      const img = new Image();
      img.onload = () => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        const aspectRatio = img.height / img.width;
        canvas.width = 500;
        canvas.height

---
**assistant**:  = 500 * aspectRatio;
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        const selectedFormat = outputFormat.value;
        const quality = selectedFormat === 'image/jpeg' ? qualityRange.value / 100 : undefined;
        const dataUrl = canvas.toDataURL(selectedFormat, quality);
        output.src = dataUrl;
        imgDataUri.value = `<img src="${dataUrl}" alt="Converted Image" />`;
        const size = Math.round(dataUrl.length * 3 / 4); // Approximate size in bytes
        jpegSize.textContent = `Image Size: ${size} bytes`;
      };
      img.src = imgSrc;
    }
  </script>
</body>
</html>

In this updated code, we added a new dropdown menu with the ID outputFormat that allows the user to select the desired output format (JPEG or PNG). The outputFormat dropdown menu has two options: "JPEG" with the value image/jpeg and "PNG" with the value image/png.

We also added an event listener for the change event on the outputFormat dropdown menu. When the selected format changes, the updateImage function is called to update the displayed image and the textarea content.

In the updateImage function, we read the selected format from the outputFormat dropdown menu and use it as the first argument to the toDataURL method. If the selected format is JPEG, we use the quality factor from the slider as the second argument to the toDataURL method. If the selected format is PNG, the quality factor is not used, and the second argument is set to undefined.

The label for the size of the image has been updated to "Image Size" to reflect that it can show the size of both JPEG and PNG images.


user: Are there quality options for PNG, or other ways to shrink its file size?


assistant: PNG is a lossless image format, which means that it preserves all the details of the original image without any loss of quality. As a result, there is no quality factor for PNG images like there is for JPEG images, which are lossy and allow for adjustable compression levels.

However, there are other ways to optimize and reduce the file size of PNG images:

  1. Color Reduction: Reducing the number of colors in the image can significantly decrease the file size. This is especially effective for images with a limited color palette, such as logos and icons.

  2. Image Resolution: Decreasing the resolution (dimensions) of the image can reduce the file size. This is useful when a smaller image size is acceptable for the intended use.

  3. Compression Tools: There are third-party tools and libraries that can optimize PNG images by applying advanced compression techniques. Some popular tools include pngquant, OptiPNG, and TinyPNG.

It's important to note that the HTML5 canvas toDataURL method does not provide built-in options for these optimizations. To apply advanced PNG optimizations, you would need to use external tools or server-side processing.

In the context of the code provided earlier, you can implement image resolution reduction by allowing the user to specify the desired width of the output image. However, for color reduction and advanced compression, you would need to use external tools or server-side processing, which is beyond the scope of client-side JavaScript and HTML.


user: Make it so clicking on the #dropzone div lets the user pick a file with a file picker


assistant: To allow the user to pick a file using a file picker when clicking on the #dropzone div, we can add an <input type="file"> element to the HTML and trigger its click event when the #dropzone div is clicked. Here's the updated code:

<!DOCTYPE html>
<html>
<head>
  <title>Drag and Drop Image</title>
  <style>
    #dropzone {
      width: 500px;
      height: 300px;
      border: 2px dashed #ccc;
      text-align: center;
      padding: 20px;
      margin-bottom: 20px;
      cursor: pointer;
    }
    #output {
      width: 500px;
      margin-bottom: 20px;
    }
    textarea {
      width: 500px;
      height: 100px;
    }
    .container {
      margin-bottom: 20px;
    }
    #fileInput {
      display: none;
    }
  </style>
</head>
<body>
  <div id="dropzone">Click or drag and drop an image here</div>
  <input type="file" id="fileInput" accept="image/*">
  <!-- Rest of the code remains the same -->
  <!-- ... -->
  
  <script>
    const dropzone = document.getElementById('dropzone');
    const fileInput = document.getElementById('fileInput');
    // Rest of the code remains the same
    // ...

    dropzone.addEventListener('click', () => {
      fileInput.click();
    });

    fileInput.addEventListener('change', () => {
      const file = fileInput.files[0];
      if (file && file.type.startsWith('image/')) {
        const reader = new FileReader();
        reader.onload = (e) => {
          imgSrc = e.target.result;
          updateImage();
        };
        reader.readAsDataURL(file);
      } else {
        alert('Please select an image file.');
      }
    });

    // Rest of the code remains the same
    // ...
  </script>
</body>
</html>

In this updated code, we added an <input type="file"> element with the ID fileInput to the HTML. The accept="image/*" attribute ensures that only image files


@simonw
Copy link
Author

simonw commented Apr 5, 2023

Here's the full resulting app:

<!DOCTYPE html>
<html>
<head>
  <title>Drag and Drop Image</title>
  <style>
    #dropzone {
      width: 500px;
      height: 100px;
      border: 2px dashed #ccc;
      text-align: center;
      padding: 20px;
      margin-bottom: 20px;
    }
    #output {
      width: 500px;
      margin-bottom: 20px;
    }
    textarea {
      width: 500px;
      height: 100px;
    }
    .container {
      margin-bottom: 20px;
    }
    #fileInput {
      display: none;
    }
  </style>
</head>
<body>
  <div id="dropzone">Click or drag and drop an image here</div>
  <input type="file" id="fileInput" accept="image/*">
  <div class="container">
    <label for="qualityRange">JPEG Quality: <span id="qualityValue">75</span></label><br>
    <input type="range" id="qualityRange" min="0" max="100" value="75" />
  </div>
  <img id="output" src="" alt="Converted Image" /><br>
  <div class="container">
    <label for="imgDataUri">Image Data URI:</label><br>
    <textarea id="imgDataUri" readonly></textarea>
  </div>
  <div id="jpegSize">JPEG Size: 0 bytes</div>

  <script>
    const dropzone = document.getElementById('dropzone');
    const fileInput = document.getElementById('fileInput');
    dropzone.addEventListener('click', () => {
      fileInput.click();
    });

    fileInput.addEventListener('change', () => {
      const file = fileInput.files[0];
      if (file && file.type.startsWith('image/')) {
        const reader = new FileReader();
        reader.onload = (e) => {
          imgSrc = e.target.result;
          updateImage();
        };
        reader.readAsDataURL(file);
      } else {
        alert('Please select an image file.');
      }
    });
    const output = document.getElementById('output');
    const imgDataUri = document.getElementById('imgDataUri');
    const qualityRange = document.getElementById('qualityRange');
    const qualityValue = document.getElementById('qualityValue');
    const jpegSize = document.getElementById('jpegSize');
    let imgSrc;

    dropzone.addEventListener('dragover', (e) => {
      e.preventDefault();
      dropzone.style.backgroundColor = '#f0f0f0';
    });

    dropzone.addEventListener('dragleave', () => {
      dropzone.style.backgroundColor = '';
    });

    dropzone.addEventListener('drop', (e) => {
      e.preventDefault();
      dropzone.style.backgroundColor = '';

      const file = e.dataTransfer.files[0];
      if (file && file.type.startsWith('image/')) {
        const reader = new FileReader();
        reader.onload = (e) => {
          imgSrc = e.target.result;
          updateImage();
        };
        reader.readAsDataURL(file);
      } else {
        alert('Please drop an image file.');
      }
    });

    qualityRange.addEventListener('input', () => {
      qualityValue.textContent = qualityRange.value;
      updateImage();
    });

    function updateImage() {
      if (!imgSrc) return;
      const img = new Image();
      img.onload = () => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        const aspectRatio = img.height / img.width;
        canvas.width = 500;
        canvas.height = 500 * aspectRatio;
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        const quality = qualityRange.value / 100;
        const dataUrl = canvas.toDataURL('image/jpeg', quality);
        output.src = dataUrl;
        imgDataUri.value = `<img src="${dataUrl}" alt="Converted Image" />`;
        const size = Math.round(dataUrl.length * 3 / 4); // Approximate size in bytes
        jpegSize.textContent = `JPEG Size: ${size.toLocaleString()} bytes`;
      };
      img.src = imgSrc;
    }
</script>
</body>
</html>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment