使用 div 能以像素级别绘制图片吗?

1,739 阅读6分钟

相信大家看到这标题首先会是一脸懵😳,啥叫像素级别绘制图片啊?其实本文想跟读者分享的是我学习图片的存储结构后的一些心得体会。

背景

我对图片是如何展示出来的很感兴趣,所以就去找相关资料学习了一下,目前网络上常见的图片格式有以下几种:jpgpnggifwebp等。这些图片格式的头信息相对复杂一些,尤其是有损压缩的图片。如果单纯想学习图片的存储结构,使用复杂的图片结构会让学习曲线变得陡峭,所以我选择了一种结构比较简单的图片格式:bmp 图片来进行学习。

BMP 图片

以下是摘抄 AI 的回答:

BMP(BitmapFileFormat)是一种最早和最简单的图片结构之一。BMP 格式最早由 Microsoft 公司在 1987 年推出,用于存储位图图像。

BMP 格式简单易懂,被广泛应用于 Windows 操作系统和其他一些平台上。但是,由于 BMP 格式没有使用压缩技术,因此文件大小通常较大,不适用于存储大型图像。此外,BMP 格式也不支持透明度和一些高级图像处理功能,因此在某些情况下可能会被其他更先进的图像格式所替代。

虽然 BMP 格式相对简单,但它仍然是一种常用的图像格式,尤其是在一些老旧的设备和应用程序中仍然被广泛使用。

人性的特点就是柿子要挑软的捏,媳妇要挑漂亮的找,好逸恶劳的千万不能要,大方让给你的好兄弟。

言归正传,接下来咱们就来捏一捏这个软柿子。

解析 BMP 图片内容

BMP 图片是以二进制格式存储的,所以叫 bitmap 位图嘛,既然是以二进制格式存储的,就可以通过每一组二进制数据来看到它要表达的内容,而二进制数据看起来是不直观的,一般都使用十六进制的格式来查看。

在 VSCode 中安装一个叫 Hex Editor 的插件,然后找一张 BMP 格式的图片,用这个插件打开它。

可以用我仓库里的图片:gitee.com/kybetter/le…

打开 BMP 图片后,你会看到图片里面是长这样的: hex-pic.png

好吧,一堆使用十六进制展示的数据,接下来就解析一下这些十六进制数据分别是代表什么意思。

BMP 文件头

文件一般都是有文件头部数据的,用来记录跟文件相关的一些信息。BMP 文件也不例外。

BMP 数据从文件头开始的先后顺序分别为:文件头、位图信息头、调色板、位图数据。

文件头记录了文件的格式、大小等信息,共14字节。

头两个字节代表文件类型(2 bytes),看上图是 42 4D,因为 BMP 图片是小端存储,所以在真正读取的时候需要转换一下,变成 4D 42

接下来是位图大小 4 bytes,36 C0 12 00,读取为 00 12 C0 36 = 1,228,854‬ bytes = 1.17MB,也就是这张图片的大小为 1.17MB。

再往后跳 4 个字节我们找到 36 00 00 00,它代表从文件头开始到实际的图像数据之间的字节的偏移量(4 bytes),读取为 00 00 00 36 = 54 bytes,即从文件头到图像数据需要偏移 54 个字节。从这个信息就可以知道如果要读取图片数据的话直接从第 55 个字节开始读取就好了。

剩下的一堆头信息我就懒得列了,大家可以查看这篇文章来学习:www.kevinnan.org.cn/index.php/a…

这里只列出对接下来绘制图片有用的信息:

biWidth	4 bytes	位图的宽度,以像素为单位
biHeight	4 bytes	位图的高度,以像素为单位
biBitCount	2 bytes	说明比特数/像素,常用的值为148162432

在本例中,biWidth 和 biHeight 都是 640px,biBitCount 是 24(表示24位真彩色)。

现在对绘制有用的数据,已知的有 biWidth 和 biHeight,可以直接拿来用。

缺少的是每行有多少个像素,要怎么得到?

我们从第55个字节开始来看图像的数据。也就是从 A3 87 51 开始,这三个字节代表 RGB 颜色,但是在 BMP 中 24 位 RGB 按照 BGR 的顺序来存储每个像素的各个颜色通道的值。 也就是:

R=51
G=87
B=A3

先不管 RGB 还是 BGR 的顺序,现在我们需要知道的是每行像素的数据有多少,也就是从哪开始换下一行。

根据这个公式: int iLineByteCnt = (((m_iImageWidth * m_iBitsPerPixel) + 31) >> 5) << 2

把 24 和 640 带入公式来计算,得到:1920,就得到了每行有 1920 个字节来代表像素,超过的就是下一行的像素。

利用这些数值,就可以将 BMP 中的数据转换成我们需要的能在前端使用的数据了。

下面是这块的代码参考:

// 计算得到 1920
lineByteCnt =
  ((parseInt(biBitCount, 16) * parseInt(biWidth, 16) + 31) >> 5) << 2;

// 转换成按行表示的数组
const bitmapDataArr = [];
for (let i = 0; i < parseInt(biHeight, 16); i++) {
  bitmapDataArr.unshift(
    bitmapData.slice(i * lineByteCnt, (i + 1) * lineByteCnt)
  );
}

// 转换成 bgr 值
const rgbArr = [];
for (let i = 0; i < bitmapDataArr.length; i++) {
  const line = bitmapDataArr[i];
  const lineArr = [];
  for (let j = 0; j < line.length; j += 3) {
    lineArr.push([line[j], line[j + 1], line[j + 2]]);
  }
  rgbArr.push(lineArr);
}

现在这个 rgbArr 里面保存的就是最终需要的数据了。 格式为:

// rgbArr
[  [[163, 135, 81], [163, 135, 81], [163, 135, 81], ...],
  [[163, 135, 81], [163, 135, 81], [163, 135, 81], ...],
]

可以看到里面就是一组组 bgr 的值,注意顺序不是 rgb,接下来就可以用它来绘制图片了。

最终的效果如下: demo.png

我分别使用了 canvas 和 div 来做示例,其中 canvas 的 createImageData 就可以很好的说明图片是按照这种像素点一点一点绘制出来的。

而用 div 逐行绘制,也是想让大家对图片的渲染绘制有个比较直观的印象。

完整代码在这个仓库,你可以下载了运行自行体验: gitee.com/kybetter/le…

因为文本的目的是要了解图片的数据结构,选用了比较简单的 BMP 图片来进行分析学习,所以文中忽略了很多 BMP 的细节,因为那些细节知识对本文的目标没有帮助,现在知道了 BMP 图片的原理,相信其它图片格式也是相通的道理,只不过需要做一些复杂的转换。

自从你学习了图片的原理以后,你拿到一张漂亮妹子的照片,一边盯着她看一边想着:噢,原来她里面是这样的。