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%}
}

 

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