图片的本质和图片压缩原理及实现

4,182 阅读7分钟

图片的本质

以下内容转载自 JPEG 图片存储格式与元数据解析

例如,一张 4px × 4px 的彩色图片,未压缩的的原始图像数据,就是一个 4 × 4 矩形网格,每一个网格代表一个像素

而彩色图片的每一个像素,又是由 红,绿,蓝 三基色构成,如下图右边所示,红绿蓝,对应于 r g b 三个数值,也就是我常说的 RGB 色彩模式。

RGB,我们在计算机视觉领域,又称为颜色通道,彩色图像有三个通道值,每个颜色通道,都是一个 0~255 的整数值,占用一个字节(Byte)的存储空间。1 个像素点需要 3 个字节

因此,我们很容易计算上面这张 4×4 彩色图片占用的存储空间为 4 × 4 × 3 = 48 字节 (Bytes) 。换算成我们熟悉的 KB,就是 48 / 1024 = 0.046875 KB,不到 0.1 KB。

事实上,我们很少见到这么小的图片,甚至在我们的个人电脑和手机上,根本无法正常看到这么小的图片。这里为了方便理解和计算,做了技术上的处理,而不是真实看到的图片大小。

拓展:按照在电脑上常用的分辨率 72 (像素/英寸),即 每 2.54 厘米 容纳 72 个像素,或者说,一个像素占用的屏幕尺寸是 0.35 毫米,那么上面 4 × 4 图片,在屏幕上 1:1 显示,占用屏幕的物理尺寸只有 1.4 × 1.4 毫米。显然,用肉眼是无法看清的。

在理解一张 4 × 4 的彩色图片占用存储空间大小,我们同样的方式计算如下,320 × 320 的彩色图片,这个大小在我们日常生活,也不算一张大图,相当于我们用作微信头像的大小。

我相信我们可以很快得出结果,320 × 320 × 3 = 300 KB ,相当于上面 4 × 4 图片的 6000 多倍。

iPhone 拍的一张图片在未压缩的情况下,所占用的存储空间大小是 3024 × 4032 × 3 = 35 MB 。而实际,如下图,在我的 Mac 上看到的图片, 只有 6.8 M ,说明我们在使用手机拍摄照片后,在保存在相册之前,相机程序已经自动对我们拍摄的照片照片进行了压缩。

图片的二进制形式

不同于普通文本文件,图片在计算机里存储形式,是二进制文件。

我们可以借助一个命令行工具 hexdump 来查看图片的二进制形式:

输出结果如下图所示:

图中,红线框圈住的部分,是图片数据的字节流编址,可以看作是为了查看方便,添加的行号,红框右边的才是图片的真实存储字节流,并且每行显示 16 个字节。当然不管是“行号”还是图片数据,为了显示的简介性,默认都是用了十六进制

  • RGB 表示形式: RGB(256,256,256)
  • 十六进制表示形式: #ffffff

图片压缩的原理

尺寸压缩

10 × 10 的像素点区域块被用最中间那一个像素点代替

编码压缩

1. 有损压缩

有损压缩是利用了人类对图像或声波中的某些频率成分不敏感的特性,允许压缩过程中损失一定的信息;虽然不能完全恢复原始数据,但是所损失的部分对理解原始图像的影响缩小,却换来了大得多的压缩比

本质和尺寸压缩本质上一样,用最中间的一个像素点代替周围的像素点

2. 行程长度编码法(无损压缩)

常用的无损压缩算法,将一扫描行中颜色值相同的相邻像素用两个字节来表示, 第一个字节是一个计数值, 用于指定像素重复的次数; 第二个字节是具体像素的值。能够比较好地保存图像的质量,但是相对有损压缩来说这种方法的压缩率比较低

例如:499 500 500 500 501 → 499 500×3 501

3. 熵编码法(无损压缩)

熵编码法是一种进行无损数据压缩的技术,在这个技术中一段文字中的每个字母被一段不同长度的比特(Bit)所代替。与此相对的是LZ77或者LZ78等数据压缩方法,在这些方法中原文的一段字母列被其它字母取代。

本质上看就是利用一个算法,把一段字母用一个或单个字母代替(端到端之间可以存一个压缩字符映射表)

例如:499 500 500 500 501 → -1 0 500 0 1

使用 Canvas 压缩图片

以下内容转载自 JS 图片压缩

压缩思路

涉及到 JS 的图片压缩,我的想法是需要用到 Canvas 的绘图能力,通过调整图片的分辨率或者绘图质量来达到图片压缩的效果,实现思路如下:

  • 获取上传 Input 中的图片对象 File
  • 将图片转换成 base64 格式
  • base64 编码的图片通过 Canvas 转换压缩,这里会用到的 Canvas 的 drawImage 以及 toDataURL 这两个 Api,一个调节图片的分辨率的,一个是调节图片压缩质量并且输出的,后续会有详细介绍
  • 转换后的图片生成对应的新图片,然后输出

base64 编码指的是把二进制变成字符的过程,base64 解码就是把字符变回二进制的过程示例:

  • 转换前 10101101,10111010,01110110
  • 按照 编码规则 转换后 00101011, 00011011 ,00101001 ,00110110
  • 十进制 43 27 41 54
  • 对应 码表 中的值 r b p 2
  • 所以上面的24位编码,编码后的Base64值为 rbp2

优缺点介绍

不过 Canvas 压缩的方式也有着自己的优缺点:

  • 优点:实现简单,参数可以配置化,自定义图片的尺寸,指定区域裁剪等等。
  • 缺点:只有 jpeg 、webp 支持原图尺寸下图片质量的调整来达到压缩图片的效果,其他图片格式,仅能通过调节尺寸来实现

代码实现

<template>
  <div class="container">
    <input type="file" id="input-img" @change="compress" />
    <a :download="fileName" :href="compressImg" >普通下载</a>
    <button @click="downloadImg">兼容 IE 下载</button>
    <div>
      <img :src="compressImg" />
    </div>
  </div>
</template>
<script>
export default {
  name: 'compress',
  data: function() {
    return {
      compressImg: null,
      fileName: null,
    };
  },
  components: {},
  methods: {
    compress() {
      // 获取文件对象
      const fileObj = document.querySelector('#input-img').files[0];
      // 获取文件名称,后续下载重命名
      this.fileName = `${new Date().getTime()}-${fileObj.name}`;
      // 获取文件后缀名
      const fileNames = fileObj.name.split('.');
      const type = fileNames[fileNames.length-1];
      // 压缩图片
      this.handleCompressImage(fileObj, type);
    },
    handleCompressImage(img, type) {
      const vm = this;
      let reader = new FileReader();
      // 读取文件
      reader.readAsDataURL(img);
      reader.onload = function(e) {
        let image = new Image(); //新建一个img标签
        image.src = e.target.result;
        image.onload = function() {
          let canvas = document.createElement('canvas');
          let context = canvas.getContext('2d');
          // 定义 canvas 大小,也就是压缩后下载的图片大小
          let imageWidth = image.width; //压缩后图片的大小
          let imageHeight = image.height;
          canvas.width = imageWidth;
          canvas.height = imageHeight;
          
          // 图片不压缩,全部加载展示
          context.drawImage(image, 0, 0);
          // 图片按压缩尺寸载入
          // let imageWidth = 500; //压缩后图片的大小
          // let imageHeight = 200;
          // context.drawImage(image, 0, 0, 500, 200);
          // 图片去截取指定位置载入
          // context.drawImage(image,100, 100, 100, 100, 0, 0, imageWidth, imageHeight);
          vm.compressImg = canvas.toDataURL(`image/${type}`);
        };
      };
    },
    // base64 图片转 blob 后下载
    downloadImg() {
      let parts = this.compressImg.split(';base64,');
      let contentType = parts[0].split(':')[1];
      let raw = window.atob(parts[1]);
      let rawLength = raw.length;
      let uInt8Array = new Uint8Array(rawLength);
      for(let i = 0; i < rawLength; ++i) {
        uInt8Array[i] = raw.charCodeAt(i);
      }
      const blob = new Blob([uInt8Array], {type: contentType});
      this.compressImg = URL.createObjectURL(blob);
      if (window.navigator.msSaveOrOpenBlob) {
        // 兼容 ie 的下载方式
        window.navigator.msSaveOrOpenBlob(blob, this.fileName);
      }else{
        const a = document.createElement('a');
        a.href = this.compressImg;
        a.setAttribute('download', this.fileName);
        a.click();
      }
    },
  }
};
</script>

上面的代码是可以直接拿来看效果的,不喜欢用 Vue 的也可以把代码稍微调整一下,下面开始具体分解一下代码的实现思路

1. Input 上传 File 处理

将 File 对象通过 FileReaderreadAsDataURL 方法转换为URL格式的字符串(base64 编码)

const fileObj = document.querySelector('#input-img').files[0];
let reader = new FileReader();
// 读取文件
reader.readAsDataURL(fileObj);

2. Canvas 处理 File 对象

建立一个 Image 对象,一个 canvas 画布,设定自己想要下载的图片尺寸,调用 drawImage 方法在 canvas 中绘制上传的图片

let image = new Image(); //新建一个img标签
image.src = e.target.result;
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
context.drawImage(image, 0, 0);

【drawImage API 解析】

context.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
  • img

就是图片对象,可以是页面上获取的 DOM 对象,也可以是虚拟 DOM 中的图片对象。

image.png

  • dx、dy、dWidth、dHeight

参数必填,用于规定在 Canvas 中绘制图片的大小和起点位置

  • sx、sy、swidth、sheight

参数选填,用于裁剪

以下为图片绘制的实例:

context.drawImage(image, 0, 0, 100, 100);
context.drawImage(image, 300, 300, 200, 200);
context.drawImage(image, 0, 100, 150, 150, 300, 0, 150, 150);

image.png

Api 中奇怪之处在于,sx、sy、swidth、sheight 为选填参数,但位置在 dx、dy、dWidth、dHeight 之前。

3. Canvas 输出图片

调用 canvastoDataURL 方法可以输出 base64 格式的图片。

canvas.toDataURL(`image/${type}`);

【toDataURL API 解析】

canvas.toDataURL(type, encoderOptions);
  • type (可选)

图片格式,默认为 image/png。

  • encoderOptions (可选)

在指定图片格式为 image/jpeg 或 image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。

4. a 标签的下载

调用 <a> 标签的 download 属性,即可完成图片的下载。

【download API 解析】

// href 下载必填
<a download="filename" href="href"> 下载 </a>
  • filename (选填):规定作为文件名来使用的文本。

  • href:文件的下载地址。