Skip to content

Latest commit

 

History

History
235 lines (181 loc) · 8.31 KB

05.md

File metadata and controls

235 lines (181 loc) · 8.31 KB

图片压缩

前言

在前端领域里,浏览器本身没有压缩图片的能力,目前业界在前端主流的图片压缩方式有两种,尺寸(像素)压缩和质量压缩。

  • 尺寸压缩: 通过用 canvas 按压缩比例绘制图片长度和宽度来压缩图片。
  • 质量压缩: canvas 绘制图片后,输出base64字符串过程,设置输出图片的质量,这里的质量算是可以理解为清晰度。

本篇文章主要利用 canvas尺寸压缩质量压缩 的结合能力来压缩图片。

原理步骤

具体实现的步骤有

  • 步骤1 (尺寸压缩): 设置尺寸压缩的参数
      1. 设定待压缩图片最大限制像素为 2000*2000 = 400万像素
      • 受限于不同浏览器对canvas总绘制图片像素大小的限制,取个保守的限制尺寸 400万像素
      1. 设定待压缩图片期待压缩过程的切成多个瓦片,每个瓦片的像素为 1000*1000 = 100万像素
      • 因为受限于部分浏览器对canvas图片一次性绘制的大小限制,保守设置一次绘制100万像素的瓦片
  • 步骤2 (尺寸压缩): 计算原始图片与最大限制尺寸(长度/宽度)的比例情况
  • 步骤3 (尺寸压缩): 如果原始图片像素长宽比限制像素尺寸大,换算出压缩结果尺寸
  • 步骤4 (尺寸压缩): 计算需要拆分的瓦片数量
  • 步骤5 (尺寸压缩): 拼接瓦片
    • 如果瓦片数量大于1,就需要拼接瓦片
      • 按照换算的瓦片位置,把瓦片原图绘制到一个临时tempCanvas里
      • 再把临时的tempCanvas根据压缩后换算的长度/宽度,x轴位置和y轴位置,绘制到结果的canvas位置上
      • 循环直至瓦片全部压缩比例绘制到结果canvas上
    • 如果瓦片数量小于1,即可以安全绘制到结果canvas里
  • 步骤6 (质量压缩): 结果的canvas按照0~1的范围内,取压缩比例,输出成图片base64

实现源码

源码地址

https://github.com/chenshenhai/canvas-note/blob/master/demo/lib/util/compress.js

源码解析

// 1.1 设定待压缩图片最大限制像素为 2000*2000 = 400万像素
// 受限于不同浏览器对canvas总绘制图片像素大小的限制,取个保守的限制尺寸 400万像素
const IMG_LIMIT_SIZE = 2000 * 2000;

// 1.2 设定待压缩图片期待压缩过程的切成多个瓦片,每个瓦片的像素为 1000*1000 = 100万像素
// 因为受限于部分浏览器对canvas图片一次性绘制的大小限制,保守设置一次绘制100万像素的瓦片
const PIECE_SIZE = 1000 * 1000;


/**
 * 压缩图片
 * @param {Image} img 
 * @param {object} opts
 *   opts.type: 输出图片类型
 *   opts.encoderOptions: 压缩比例,范围在[0,1],只在 type='image/jpeg'时候有效
 * @return {string} 输出类型
 */
export const compressImage = function(img, opts = { type: 'image/jpeg',  encoderOptions: 0.5 }) {
  const {type, encoderOptions } = opts;
  const w = img.width;
  const h = img.height;
  let outputW = w;
  let outputH = h;

  // 获取原始图片尺寸
  let imageSize = w * h;
  // 2. 计算原始图片与最大限制尺寸(长度/宽度)的比例情况
  // 由于是面积换算比例,所以要取开平方才能清晰知道原始像素为最大限制像素的长度/宽度比例
  // 例如: 原始图片像素为  8000 * 8000 = 64,000,000 六千四百万像素
  //      是最大图片限制  2000 * 2000 像素的 长度/宽度的四倍
  let ratio = Math.ceil(Math.sqrt(Math.ceil(imageSize / IMG_LIMIT_SIZE)));
  
  if ( ratio > 1) {
    // 如果原始图片像素长宽比限制像素尺寸大
    // 就换算出压缩后图片尺寸的长度和宽度 
    outputW = w / ratio;
    outputH = h / ratio;
  } else {
    // 剩下情况都是比例为1,即无需压缩,原样输出
    ratio = 1;
  }

  let canvas = document.createElement('canvas');
  let tempCanvas = document.createElement('canvas');
  let context = canvas.getContext('2d');
  canvas.width = outputW;
  canvas.height = outputH;
  context.fillStyle = '#FFFFFF';
  context.fillRect(0, 0, canvas.width, canvas.height);

  // 计算需要拆分的瓦片数量
  const pieceCount = Math.ceil(imageSize / PIECE_SIZE);

  if (pieceCount > 1) {
    // 如果瓦片数量大于1,就需要进行瓦片绘制到一个临时tempCanvas里
    // 再把临时的tempCanvas根据压缩后换算的长度/宽度,x轴位置和y轴位置,绘制到结果的canvas位置上
    // 直到所有瓦片按照结果尺寸和瓦片数量拼接完毕

    const pieceW = Math.ceil(canvas.width / pieceCount);
    const pieceH = Math.ceil(canvas.height / pieceCount);

    tempCanvas.width = pieceW;
    tempCanvas.height = pieceH;
    let tempContext = tempCanvas.getContext('2d');

    const sw = pieceW * ratio;
    const sh = pieceH * ratio;
    const dw = pieceW;
    const dh = pieceH;
    for(let i = 0; i < pieceCount; i++) {
      for(let j = 0; j < pieceCount; j++) {
        const sx = i * pieceW * ratio;
        const sy = j * pieceH * ratio;
        tempContext.drawImage(img, sx, sy, sw, sh, 0, 0, dw, dh);
        context.drawImage(tempCanvas, i * pieceW, j * pieceH, dw, dh);
      }
    }

    tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
    tempCanvas.width = 0;
    tempCanvas.height = 0;
    tempCanvas = null;
  } else {
    // 如果瓦片数量小于1,即可以安全绘制到结果canvas里
    context.drawImage(img, 0, 0, outputW, outputH);
  }

  // 将结果的canvas输出成base64
  // 上述压缩的是尺寸,这里使用 encoderOptions 压缩的是质量,可以理解为清晰度
  const base64 = canvas.toDataURL(type, encoderOptions);
  context.clearRect(0, 0, canvas.width, canvas.height);
  canvas.width = 0;
  canvas.height = 0;
  canvas = null;

  return base64;
}

测试例子

例子源码地址

https://github.com/chenshenhai/canvas-note/blob/master/demo/chapter-03/05/

例子源码详解

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>chapter</title>
  <link rel="stylesheet" href="./css/index.css">
</head>
<body>
  
  <div class="canvas-list">
    <div class="canvas-bg">
      <canvas id="canvas-1"></canvas>
    </div>
    <div class="canvas-bg">
      <canvas id="canvas-2"></canvas>
    </div>
  </div>
  <div id="info"></div>

  <script type="module" src="./js/index.js"></script>
</body>
</html>
import { getImageBySrc } from './../../../lib/util/file.js';
import { compressImage } from './../../../lib/util/compress.js';
 
(async function() {

  // 绘制原始图像
  const canvas1 = document.getElementById('canvas-1');
  const ctx1 = canvas1.getContext('2d');
  const img = await getImageBySrc('./../../image/pexels-photo-001.jpg');
  canvas1.width = img.width;
  canvas1.height = img.height;
  ctx1.drawImage(img, 0, 0);


  // 绘制压缩后图像
  const canvas2 = document.getElementById('canvas-2');
  const ctx2 = canvas2.getContext('2d');
  const compressedImgSrc = compressImage(img);
  const timeBefore = new Date().getTime();
  const compressedImg = await getImageBySrc(compressedImgSrc);
  const timeAfter = new Date().getTime();
  canvas2.width = compressedImg.width;
  canvas2.height = compressedImg.height;
  ctx2.drawImage(compressedImg, 0, 0);

  // 前后尺寸结果
  const originSize = img.width * img.height;
  const compressedSize = compressedImg.width * compressedImg.height;
  const infoText = `
   原始尺寸大小: ${img.width} * ${img.height} = ${originSize} 像素
   <br>
   压缩后尺寸大小: ${compressedImg.width} * ${compressedImg.height} = ${compressedSize} 像素
   <br/>
   压缩过程耗时: ${timeAfter - timeBefore} ms
  `
  document.getElementById('info').innerHTML = infoText;
  // console.log('originSize = ', originSize);
  // console.log('compressedSize = ', compressedSize);
})();

例子结果

图片尺寸(像素)压缩结果

image

图片质量尺寸(像素)压缩 + 质量压缩结果对比

image

注意

无论是质量压缩还是尺寸压缩,在不同浏览器处理的效果都有一定的差距,压缩结果的大小不一定一致。