聊一聊前端开发中既熟悉又陌生的图片

4,109 阅读5分钟

本文首发于公众号:符合预期的CoyPan

图片的分类

图片一共可以分为两类,一类是以svg为代表的矢量图形,是用点、直线或者多边形等基于数学方程的几何图元表示图像。矢量图不论显示画面的大小,由于其记录的是“画出图片的步骤”,所以不会失真。更进一步的内容,本文就不再深入了。

另一类图片,就是位图(也叫点阵图),使用像素排列形成的图片。我们常见的png,jpg等图片就是位图。本文的内容均围绕位图展开

图片中究竟存了什么

我们看到的图片,其实是由一个一个的点组成的。这些点,被称为像素点,每一个像素点一种颜色。像下面这样

image-20220402163116061.png

随便找一张图片进行放大,可以看出来。

wecom-temp-cfda578a80c579eba99db0066ae10a28.png

如果一张图片长10英寸 ,宽10英寸,那这种图片包含多少个像素点呢?这还和图片的分辨率(dpi)有关。dpi,即图像每英寸长度内的像素点数。

像素 = 尺寸 * 分辨率

互联网中的图片一般是72dpi和96dpi。10英寸 * 10英寸的图片,如果dpi是72,那就是720 * 720个像素点。使用操作系统的图片查看器,就能看到图片的这些信息了。

image-20220402173356766.png

从上面的图片信息里面,我们还可以看到一个【深度】。这里的深度指的是图片的位深度

图片的位深度表示的是用多少个二进制位来记录像素点的颜色。例如位深度为8,即表示用8个bit来表示一个像素点的颜色。一共可以表示 2^8 = 256种颜色。

常见的位深度有:

  • 1位。黑白照片,只有黑色和白色
  • 4位。有2的4次幂种颜色,16种颜色
  • 8位。2的8次幂表示,它含有256种颜色(VGA)
  • 24位。2的24次幂种颜色,16777216种颜色(真彩色, SVGA)
  • 32位。32位在24位基础上加了透明度,颜色数量和24位是一样的.

说到这里,就不得不先提到颜色。最常用的颜色空间就是RGB了。RGB颜色空间中, 我们认为所有的颜色都是由红、绿、蓝 ( RGB ) 三基色组成的,任何一种颜色都可以转换成这三种基本颜色的混合,只是各基色的比例不同而已。使用RGB颜色空间来表示颜色时,每一种基色都用8个bit来表示,3种基色一共需要8*3=24个bit,也就是需要用24位才能表示一种颜色。例如

0000 0000 0000 0000 0000 0000 - 黑色 #000000
1111 1111 1111 1111 1111 1111 - 白色 #ffffff

那么按照这种方式的话,如果想要完整描述一张100像素 * 100像素的图片,那岂不是需要:

8 * 3 * 100 * 100 = 240000 bit = 30000 Bytes = 29.296875 KB

一张 100 * 100的图片会有这么大么?这就涉及到图片的格式了。不同格式的图片,会采用不同的编码方式对原始图片数据进行编码、压缩等操作。最终我们看到的图片,如jpg,png,gif等,都是经过一系列处理后得到的产物。至于怎么进行压缩,每一种图片格式有什么区别,网上有很多不错的文章已经讲得挺清楚了,这里就不展开了。

前端js代码操作图片

判断图片类型

前端如何判断图片的类型呢?根据后缀是不靠谱的。因为后缀是可以随意更改的,但是更改了后缀后,并不能改变图片本身的格式。比如我把一张png图片直接改成了jpg后缀,查看其类型,依然是png类型。

image-20220403231512109.png

每一种格式的图片,都是以二进制形式存储的。下面就是一张图片中的二进制数据(以16进制表示的)。

image-20220404000829799.png

二进制数据的头几个字节,标识了图片的元数据信息,这其中,就包括了图片的格式。每一种图片格式对应的元数据是固定的(可以称之为“魔数”),我们可以通过这个点来判断图片格式。

比如,png图片的魔数为:[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],jpg图片的魔数为:[0xff, 0xd8, 0xff, 0xe0]。以png为例,通过<input type="file">选择一张图片后,判断该图片是否是png的代码可以参考:

// 读取指定长度字节
function readBytes(sBuffer, begin, length) {
  return Array.prototype.slice.call(new Uint8Array(sBuffer, begin, begin + length));
}

const reader = new FileReader();
reader.onload = (e) => {
  const buffer = e.target.result;    
  const header = readBytes(buffer, 0, 8); 
  // png的魔数
  const magicNumber = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
  // 图片的头8个字节,就可以判断是否为png图片
  let isPng = true;
  for (let i = 0; i <= 7; i++) {
    if(header[i] !== magicNumber[i]) {
      isPng = false;
    }
  }
};
// file是通过input的onChange拿到的图片file对象,通过readAsArrayBuffer可以将file对象转化为ArrayBuffer
reader.readAsArrayBuffer(file);

这里的ArrayBuffer 就是原始的二进制数据了。但是我们没法直接操作它,需要使用Uint8Array建立一个类数组试图才可以。

对于直接给出http链接的图片,可以直接使用以下代码,也能拿到其原始二进制数据:

fetch(url).then(function(res){return res.arrayBuffer();});

这里有一个点需要注意,通过 FileReader.readAsDataURL 可以将图片文件读取为base64,而base64的开头也有图片格式信息。这个信息不能准确表示该图片的格式:

image-20220404001003114.png

大多数情况下,我们只需要使用文件后缀来识别图片格式即可。某些情况下(比如需要在前端对图片进行压缩,采用不同算法的时候),或许需要采用读取二进制魔数的方式来判断图片格式了,npm上有不少的库可以使用。

canvas读取图片

前端开发中,在对图片进行处理的时候,常常使用canvas来获取图片的原始数据。

 ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, image.width, image.height);
 ctx.getImageData(0, 0, image.width, image.height);

先将图片画到canvas上,然后通过getImageData来获取canvas上图片的像素信息。getImageData返回的是ImageData对象:

- ImageData.data 只读
	Uint8ClampedArray 描述了一个一维数组,包含以 RGBA 顺序的数据,数据使用  0255(包含)的整数表示。 
- ImageData.height 只读
	无符号长整型(unsigned long),使用像素描述 ImageData 的实际高度。
- ImageData.width 只读
	无符号长整型(unsigned long),使用像素描述 ImageData 的实际宽度。

image-20220405144232909.png

需要注意的是,这里的data与图片的原始数据arrayBuffer是不一样的。任意格式的图片在画到canvas上后,都会变成相同的数据格式。浏览器在通过canvas画出图像时进行了一些处理(比如解析图片等),具体的细节,欢迎知道的小伙伴指教。

ImageData.data里面的一维数组,每四个元素表示一个像素点。

image-20220405145446308.png

获取到图片每一个像素点信息后,就可以很灵活的进行操作了,比如反转颜色、图片置灰等等。

小结

图片(这里特指位图)的本质就是一个一个的像素点的拼接,我们可以通过其二进制数据,并结合不同的图片格式来深入了解。而当前端需要对图片进行处理时,往往需要canvas来作为辅助。当需要对图片进行更进一步复杂的操作、涉及到大量计算时(比如压缩),还需要借助webAssembly。之前花了不少的时间,大概跑通了将图片压缩算法编译到webAssembly的过程,有机会再进行总结吧。