canvas 壓縮圖片

_clai發表於2024-04-24

拖動 range, 檢視壓縮效果

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        padding: 20px;
      }
      .img-preview {
        --clip-width: 0px;

        display: none;
        /* display: grid; */
        position: relative;
        grid-template-columns: 1fr 1fr;
        margin-top: 20px;
        gap: 10px;
        width: 500px;
      }
      img {
        position: absolute;
        inset: 0;
        width: 100%;
        height: auto;
        padding: 10px;
        border: 1px solid #ccc;
        border-radius: 6px;
        user-select: none;
      }
      #source-img {
        clip: rect(auto, var(--clip-width), auto, auto);
        z-index: 2;
      }
      .line {
        --line-h: 0px;
        --line-x: 0px;

        position: absolute;
        top: 0;
        left: 0;
        width: 10px;
        height: var(--line-h);
        translate: var(--line-x) 0;
        background-color: rgba(153, 153, 153, 0.5);
        z-index: 3;
        cursor: move;
        user-select: none;
      }
    </style>
  </head>
  <body>
    <input type="file" id="file" />
    <br /><br />
    <input type="range" id="quality" min="0" max="1" step="0.1" value="0.9" />
    <br />
    <div class="img-preview">
      <img alt="" id="source-img" />
      <img alt="" id="compressed-img" />
      <div class="line"></div>
    </div>

    <script>
      const imgPreview = document.querySelector('.img-preview');
      const line = imgPreview.querySelector('.line');

      init();
      function init() {
        const file = document.getElementById('file');

        file.addEventListener('change', handleImageCompress);
        line.addEventListener('mousedown', handleLineMovedown);
      }

      function handleLineMovedown(e) {
        imgPreview.addEventListener('mousemove', handleImgPreviewMove);
        line.addEventListener('mouseup', handleLineMouseup);
      }
      function handleImgPreviewMove(e) {
        let x = e.clientX - imgPreview.offsetLeft - line.offsetWidth / 2;
        x = Math.max(x, 0);
        x = Math.min(x, imgPreview.offsetWidth - line.offsetWidth);
        line.style.setProperty('--line-x', `${x}px`);
        imgPreview.style.setProperty('--clip-width', `${x}px`);
      }
      function handleLineMouseup() {
        imgPreview.removeEventListener('mousemove', handleImgPreviewMove);
        line.removeEventListener('mouseup', handleLineMouseup);
      }

      async function handleImageCompress(e) {
        /** @type {File} */
        const file = e.target.files[0];

        if (!file) return;

        const imgPreview = document.querySelector('.img-preview');
        imgPreview.style.display = 'none';

        // 讀取圖片的base64資料
        const sourceSrc = await readFileDataUrl(file);
        // 顯示圖片
        const sourceImg = document.getElementById('source-img');
        sourceImg.onload = () => {
          const compressedImg = document.getElementById('compressed-img');
          const qualityRange = document.getElementById('quality');
          const line = document.querySelector('.line');

          qualityRange.value = 0.9;

          const canvas = createCanvas(sourceImg);

          queueMicrotask(() => {
            line.style.setProperty('--line-h', `${sourceImg.offsetHeight}px`);
            const x = (sourceImg.offsetWidth - line.offsetWidth) / 2;
            line.style.setProperty('--line-x', `${x}px`);
            imgPreview.style.setProperty('--clip-width', `${x}px`);
          });

          qualityRange.addEventListener('change', (e) => {
            const quality = +e.target.value;
            // 壓縮圖片
            const compressedSrc = doCompression2(canvas, sourceSrc, file.type, quality);
            // 顯示壓縮後的圖片
            compressedImg.src = compressedSrc;
          });
          // 壓縮圖片

          const compressedSrc = doCompression(canvas, sourceSrc, file.type);
          // 顯示壓縮後的圖片
          compressedImg.src = compressedSrc;

          imgPreview.style.display = 'grid';
        };
        sourceImg.src = sourceSrc;
      }

      /**
       * 建立 canvas 並繪製圖片
       * @param {HTMLImageElement} img
       */
      function createCanvas(img) {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        canvas.width = img.naturalWidth;
        canvas.height = img.naturalHeight;
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

        return canvas;
      }

      function doCompression2(canvas, imgSrc, type, quality = 0.9) {
        const compressedSrc = canvas.toDataURL(type, quality);
        console.log('doCompression2 => ', compressedSrc.length, imgSrc.length, type, quality);
        return compressedSrc;
      }

      /**
       * 壓縮處理
       * @param {HTMLCanvasElement} canvas
       * @param {HTMLImageElement} img
       * @param {String} filetype
       * @param {Number} quality
       */
      function doCompression(canvas, imgSrc, type, quality = 0.9) {
        const compressedSrc = canvas.toDataURL(type, quality);

        // 判斷壓縮後的base64資料是否大於原圖的base64資料
        if (compressedSrc.length >= imgSrc.length) {
          if (quality <= 0) {
            return doCompression(canvas, imgSrc, 'image/webp');
          }
          return doCompression(canvas, imgSrc, type, Number((quality - 0.1).toFixed(1)));
        }
        console.log('doCompression => ', compressedSrc.length, imgSrc.length, type, quality);
        return compressedSrc;
      }

      function readFileDataUrl(file) {
        return new Promise((resolve) => {
          const reader = new FileReader();
          reader.addEventListener('load', () => {
            resolve(reader.result);
          });
          reader.readAsDataURL(file);
        });
      }
    </script>
  </body>
</html>

相關文章