js壓縮圖片到指定大小

需求:前端上傳圖片的時候通常需要提供指定大小以內的圖片。比如不大於500KB。

思路:利用canvas轉blob的時候通過quality控制圖片質量,達到壓縮的目的。此方法有個缺點。只能對圖片格式爲jpeg或webp的圖片有效。因此壓縮的時候canvas.toBlob(callback, mimeType, quality)中的mimeType要設爲'image/jpeg'。壓縮完成可以自行轉成想要的格式。這裏最主要的是找到小於maxSize並且最接近maxSize的圖片質量參數quality。

效果圖:用進度條模擬壓縮的進度。支持同時上傳多張圖片同時壓縮

 

 代碼如下:

import React from 'react';
import PropTypes from 'prop-types';
import styles from './upload.less';

import compress from './compress';

class Upload extends React.Component {
  constructor(props) {
    super(props);
    this.fileInput = React.createRef();
    this.state = {
      fileObjs: [], // item { originFile, compressBase64, compressFile }
    };
  }

  getFileUrl(file) {
    let url;
    const agent = navigator.userAgent;
    if (agent.indexOf('MSIE') >= 1) {
      url = file.value;
    } else if (agent.indexOf('Firefox') > 0 || agent.indexOf('Chrome') > 0) {
      url = window.URL.createObjectURL(file);
    }
    return url;
  }

  compressCallBack(file, fileObj, result) {
    const { fileObjs } = this.state;
    file.compressing = false; // 壓縮完成
    fileObj.compressBase64 = result.compressBase64;
    fileObj.compressFile = result.compressFile;
    this.setState({ fileObjs: [...fileObjs] });
    if (fileObjs.length && fileObjs.every(fileObjItem => fileObjItem.compressBase64)) {
      console.log('全部壓縮完成', fileObjs);
    }
  }

  onInputChange(e) {
    const { fileObjs } = this.state;
    Object.keys(e.target.files).forEach((key) => {
      const file = e.target.files[key];

      // 驗證圖片格式
      const type = file.name.split('.')[1];
      if (type !== 'png' && type !== 'jpg' && type !== 'jpeg') {
        console.warn('請上傳png,jpg,jpeg格式的圖片!');
        e.target.value = '';
        return;
      }

      file.url = this.getFileUrl(file);
      file.compressing = true; // 壓縮狀態,開始壓縮

      const fileObj = { originFile: file, compressBase64: null, compressFile: null };
      fileObjs.push(fileObj);

      // 壓縮圖片的方法, maxSize單位爲kb
      compress(file, 200).then((res) => {
        this.compressCallBack(file, fileObj, res);
      }, (err) => {
        // 壓縮失敗,則返回原圖片的信息
        this.compressCallBack(file, fileObj, err);
      });
    });

    this.setState({ fileObjs: [...fileObjs] });
    e.target.value = '';
  }

  render() {
    const { fileObjs } = this.state;
    return (
      <div
        className={styles.uploadContainer}
      >
        <div
          className={styles.inputContainer}
          onClick={() => {
            this.fileInput.current.click();
          }}
        >
          <span className={styles.uploadIcon}>+</span>
          <input
            className={styles.fileInput}
            ref={this.fileInput}
            type="file"
            name="file"
            multiple="multiple"
            accept="image/png,image/jpg,image/jpeg"
            onChange={e => this.onInputChange(e)}
          />
        </div>
        {
          fileObjs.map(fileObj => (
            <div className={styles.imgContainer}>
              <img
                src={fileObj.compressBase64 ? fileObj.compressBase64 : fileObj.originFile.url}
                className={fileObj.originFile.compressing && styles.filter}
              />
              {
                fileObj.originFile.compressing ?
                  <div className={styles.progressContainer}>
                    <div className={styles.progress}>
                      <div className={styles.progressHighlight} />
                    </div>
                  </div> : ''
              }
            </div>
          ))
        }
      </div>);
  }
}


export default Upload;

2.圖片壓縮主要代碼compress.js


// 將File(Blob)對象轉變爲一個dataURL字符串, 即base64格式
const fileToDataURL = file => new Promise((resolve) => {
  const reader = new FileReader();
  reader.onloadend = e => resolve(e.target.result);
  reader.readAsDataURL(file);
});

// 將dataURL字符串轉變爲image對象,即base64轉img對象
const dataURLToImage = dataURL => new Promise((resolve) => {
  const img = new Image();
  img.onload = () => resolve(img);
  img.src = dataURL;
});

// 將一個canvas對象轉變爲一個File(Blob)對象
const canvastoFile = (canvas, type, quality) => new Promise(resolve =>
  canvas.toBlob(blob => resolve(blob), type, quality));

const compress = (originfile, maxSize) => new Promise(async (resolve, reject) => {
  const originSize = originfile.size / 1024; // 單位爲kb
  // 將原圖片轉換成base64
  const base64 = await fileToDataURL(originfile);

  // 縮放圖片需要的canvas
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');

  // 小於maxSize,則不需要壓縮,直接返回
  if (originSize < maxSize) {
    resolve({ compressBase64: base64, compressFile: originfile });
    console.log(`圖片小於指定大小:${maxSize}KB,不用壓縮`);
    return;
  }


  const img = await dataURLToImage(base64);

  const scale = 1;
  const originWidth = img.width;
  const originHeight = img.height;
  const targetWidth = originWidth * scale;
  const targetHeight = originHeight * scale;

  canvas.width = targetWidth;
  canvas.height = targetHeight;
  context.clearRect(0, 0, targetWidth, targetHeight);
  context.drawImage(img, 0, 0, targetWidth, targetHeight);

  // 將Canvas對象轉變爲dataURL字符串,即壓縮後圖片的base64格式
  // const compressedBase64 = canvas.toDataURL('image/jpeg', 0.1);
  // 經過我的對比,通過scale控制圖片的拉伸來壓縮圖片,能夠壓縮jpg,png等格式的圖片
  // 通過canvastoFile方法傳遞quality來壓縮圖片,只能壓縮jpeg類型的圖片,png等格式不支持
  // scale的壓縮效果沒有canvastoFile好
  // 在壓縮到指定大小時,通過scale壓縮的圖片比通過quality壓縮的圖片模糊的多
  // 壓縮的思路:由於quality參數的精度爲0.01,因此我們只需要從0.9開始,一直遞減:0.8, 0.7, 0.6, 0.5,
  // 0.4, 0.3, 0.2, 0.1, 0;找到第一個小於maxSize的quality。假如第一個小於maxSize的quality爲0.4,則
  // 繼續遞增0.41, 0.42, 0.43, 0.44,當遞增到0.45是,壓縮的圖片開始大於maxSize,則此時我們可以斷定
  // quality = 0.44時,壓縮出來的圖片大小最接近maxSize。這種算法比較笨,但是也能涵蓋所有的情況。
  // 這裏爲了規避浮點數計算的弊端,將quality轉爲整數再計算;
  let preQuality = 100;
  let quality = 90;
  let count = 0; // 嘗試壓縮次數
  let compressFinish = false; // 壓縮完成
  let invalidDesc = '';
  let compressBlob = null;

  // 找到最接近maxSize的quality
  while (!compressFinish) {
    compressBlob = await canvastoFile(canvas, 'image/jpeg', quality / 100);
    const compressSize = compressBlob.size / 1024;
    count++;
    console.log(quality / 100, compressSize);
    if (maxSize >= compressSize) {
      preQuality = quality;
      quality += 1;
    } else {
      if (preQuality !== 100) {
        compressFinish = true;
      }
      if (!quality) {
        // 當quality等於0,並且壓縮後的圖片還是比指定的大小大,說明無法壓縮
        compressFinish = true;
        invalidDesc = '壓縮失敗,無法壓縮到指定大小';
      }
      quality -= 10;
    }
  }

  if (invalidDesc) {
    // 壓縮失敗,則返回原始圖片的信息
    console.log(`壓縮失敗,無法壓縮到指定大小:${maxSize}KB`)
    reject({ msg: invalidDesc, compressBase64: base64, compressFile: originfile });
    return;
  }

  compressBlob = await canvastoFile(canvas, 'image/jpeg', preQuality / 100);

  const compressedBase64 = await fileToDataURL(compressBlob);

  const compressedFile = new File([compressBlob], originfile.name, { type: 'image/jpeg' });

  console.log(`壓縮完成,總共嘗試了${count}次`);
  resolve({ compressFile: compressedFile, compressBase64: compressedBase64 });
});


export default compress;

3.less

.uploadContainer{
  display: flex;
  flex-wrap: wrap;
  .inputContainer{
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100px;
    height: 100px;
    //background:rgba(245,250,255,1);
    border-radius:8px;
    border:1px solid rgba(217,217,217,1);
    margin-right: 10px;
    .fileInput{
      display: none;
    }
    .uploadIcon{
      font-size: 30px;
      color: lightgrey;
    }
  }
  .imgContainer{
    position: relative;
    width: 100px;
    height: 100px;
    margin-right: 10px;
    &:last-child{
      margin-right: 10px;
    }
    img{
      width: 100%;
      height: 100%;
    }
    .filter{
      filter: blur(1px);
    }
    .progressContainer{
      position: absolute;
      width: 80%;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      font-size: 10px;
      .progress{
        width: 100%;
        height: 4px;
        border-radius: 3px;
        border: 1px solid rgba(0,0,0,0.1);
      }
      .progressHighlight{
        height: 100%;
        width: 100%;
        animation: progress 3s cubic-bezier(0.25,0.1,0.25,1) infinite;
        background: orange;
        border-radius: 3px;
      }
    }
  }
}


@keyframes progress
{
  0%   {width: 0}
  to  {width: 100%}
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章