平时工作中我们会遇到各种各样的图片,常规的图片分为两种格式,即位图和矢量图。位图就是根据像素进行展示,一个像素点中包含了颜色,明暗度和透明度等信息。我们常见的位图格式有 BMP, PNG,JPG,GIF 等。
一 BMP图片
最原始的图像格式,完全将像素元素转换成数据,以下面的图片为例。由于 BMP 格式完全存储了每一个像素的所有数据,通常它占用较多的内存,我们可以通过 图片格式转换器 将一副普通图片转换成 BMP 格式图片。
一幅图片常见的原始数据包括尺寸,分辨率(dpi,lpi,ppi,PPD 等),大小,色彩空间(RGB,CMY,LAB,YUV,HSI 等等)。
在 BMP 格式文件中,前 14 个字节主要是描述文件信息结构体
字段 | 地址段(字节) | 描述 |
---|---|---|
file_type | 0 ~ 2 | 文件标识,表明文件格式 |
file_size | 2 ~ 6 | 文件大小 |
reserved1 | 6 ~ 8 | 保留字 1 |
reserved2 | 8 ~ 10 | 保留字 2 |
offset_bits | 10~14 | 偏移量 |
然后 40 个字节主要是图像显示信息
字段 | 地址段(字节) | 描述 |
---|---|---|
bitmap_info_size | 14 ~ 18 | 位图信息的大小 |
bitmap_width | 18 ~ 22 | 位图宽度 |
bitmap_height | 22 ~ 26 | 位图高度 |
planes | 26 ~ 28 | 位图的位面数 |
image_depth | 28~30 | 位图的图像深度,表示位图数据中,几个二进制位表示一个像素点 |
compression | 30~34 | 位图压缩方式 |
image_size | 34~38 | 位图的数据大小 |
x_pels_permeter | 38 ~ 42 | 指定位图目标设备的水平打印分辨率 |
y_pels_permeter | 42 ~ 46 | 指定位图目标设备的垂直打印分辨率 |
color_used | 46 ~ 50 | 位图实际使用调色板的颜色数量 |
color_important | 50 ~ 54 | 重要的颜色数量 |
1.1 图像数据解析
我们可以通过 node 调用 fs 模块获取文件原始数据:
const originData = fs.readFileSync('../source/koubei.bmp');
默认 fs 通过 8 进制的形式进行内容输出,为了方便我们查看,将数据转换为 16 进制显示。
const data = origin.toString('hex');
截取部分图像数据
424d46881200000000003600000028000000d6010000860200000100200000000000000000002516000025160000000000000000000086d03aff86d03aff86d13aff86d13aff85d03aff85d03aff86d13aff86d13aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff84d03aff84d03aff85d03aff85d03aff84d03aff84d03aff85d03aff85d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d039ff84d039ff84d03aff84d03aff84d03aff84d03aff84d039ff84d039ff84d03aff84d03aff84d039ff84d039ff84d039ff84d039ff84d039ff84d039ff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff82d036ff82d036ff80cf31ff80cf31ff80d032ff80d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff81d032ff81d032ff80cf31ff80cf31ff81d032ff81d032ff80cf31ff80cf31ff80cf31ff80cf31ff80d032ff80d032ff80cf31ff80cf31ff80d032ff80d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff80d032ff80d032ff80d032ff80d
注意在 BMP 文件中数据存储方式为小端存储模式,我们习惯于按大端存储来读取数据,例如上面这幅图描述文件大小的字符串为"46881200",实际大小为 0X00128846。这里实现一个简易字符串转换函数方便后面使用
// 将小端存储的字符串转换成大端存储
const transformBit = (str) => {
let string = '';
for (var i = 0; i < str.length; i++) {
if (i % 2 == 0) {
string = string + (str[str.length - 2 - i] + str[str.length - 1 - i]);
}
}
return string;
};
通过 transformBit 函数计算文件大小。由于 MAC 硬盘中采用 1000 进位,而 Windows 采用 1024 进位,所以这个文件在 MAC 上显示 1.2M,而 Windows 上则只有 1.158M。
图像原始数据 54 位以后所有数据都是 BMP 图像的显示数据了,以上面的图片为例,我们获取原始数据的 compression 为 0,表示无压缩。 image_depth 数据得到 0X002000 , 换算下来是 8 位表示一个像素点。在这个像素点中,通常是按照颜色分量 R,G,B 和一位保留字来存储的。
1.2 图像显示
在继续获取到其他头部信息后,我们使用 canvas 来实现一个简易的 BMP 图片浏览器。
// canvas 描点
let draw = (x, y, color) => {
ctx.beginPath();
ctx.fillStyle = '#' + color;
ctx.rect(x, y, x + 1, y + 1);
ctx.fill();
ctx.closePath();
};
// 图像转换成二维数组
let imgArr = [[]];
for (let i = 0; i < imgData.length; i+=8) {
if (x >= width) {
x = 0;
y++;
imgArr[y] = [];
}
// 拼装数据
let color = imgData.substring(i, i + 6);
color = transformBit(color);
imgArr[y][x] = color;
x++;
}
// 正常显示图片
for (let j = 0; j < y; j++) {
for (let i = 0; i < x; i++) {
let color = imgArr[j][i];
draw(i, j, color);
}
}
1.3 图像处理
灰度处理
灰度计算通常有平均值法和加权平均值法,加权平均值法是重新分配三种颜色的权重,比例为 R(30%) G(59%) B(11%),对于人体视觉最为友好。这里示例直接用平均值法实现。
// 转换成黑白色
const getAverage = (color) => {
let R = parseInt(color.slice(0, 2), 16);
let G = parseInt(color.slice(2, 4), 16);
let B = parseInt(color.slice(4, 6), 16);
let average = parseInt((R + G + B) / 3);
average = average > 15 ? average.toString(16) : '0' + average.toString(16);
return average + average + average;
};
...
color = getAverage(color);
浮雕效果处理
浮雕效果实现有多种算法,实现效果也不一样。这里使用斜角相邻点差值计算的方式:
for (let j = 1; j < y - 1; j++) {
for (let i = 1; i < x - 1; i++) {
const leftTop = imgArr[j - 1][i - 1];
const rightBottom = imgArr[j + 1][i + 1];
const R =
parseInt(leftTop.slice(0, 2), 16) - parseInt(rightBottom.slice(0, 2), 16) + 128;
const G =
parseInt(leftTop.slice(2, 4), 16) - parseInt(rightBottom.slice(2, 4), 16) + 128;
const B =
parseInt(leftTop.slice(4, 6), 16) - parseInt(rightBottom.slice(4, 6), 16) + 128;
let average = parseInt((R + G + B) / 3);
if (average < 0) average = 0;
if (average > 255) average = 255;
average = average > 15 ? average.toString(16) : '0' + average.toString(16);
let color = average + average + average;
draw(i, j, color);
}
}
要实现旋转,底片,缩放,通道修改等原理都比较简单,这里不再过多罗列。
二 base64 图片
当我们开发中遇到比较小的图片时,一般 10K 以下,可以将图片直接转换成 base64 格式数据放入 src 解析。
在 Mac 下,我们可以通过 openssl 工具来进行 base64 的编解码
openssl base64 -d -in <infile> -out <outfile> //解码
openssl base64 -in <infile> -out <outfile> //编码
2.1 图像数据解析
被 base64 编码的图片数据大致这样
data:image/bmp;base64,Qk1GiBIAAAAAADYAAAAoAAAA1gEAAIYCAAABACAAAAAAAAAAAAAlFgAAJRYAAAAAAAAAAAAAhtA6/4bQOv+G0Tr/htE6/4XQOv+F0Dr/htE6/4bROv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+E0Dr/hNA6/4XQOv+F0Dr/hNA6/4TQOv+F0Dr/hdA6/4TQOv+E0Dr/hNA6/4TQOv+E0Dr/hNA6/4TQOv+E0Dr/hNA6/4TQOv+E0Dn/hNA5/4TQOv+E0Dr/hNA6/4TQOv+E0Dn/hNA5/4TQOv+E0Dr/hNA5/4TQOf+E0Dn/hNA5/4TQOf+E0Dn/hNA6/4TQOv+E0Dr/hNA6/4TQOv+E0Dr/gtA2/4LQNv+AzzH/gM8x/4DQMv+A0DL/gNAy/4DQMv+B0DL/gdAy/4DQMv+A0DL/gdAy/4HQMv+B0DL/gdAy/4DQMv+A0DL/gdAy/4HQMv+A0DL/gNAy/4DQMv+A0DL/gdAy/
前面 data 指明数据格式,base64 表示数据编码格式。其余部分为图片原始数据,由于 http 传输内容必须为字符,所以数据传输时采用 ASCII 中"A-Z、a-z、0-9、+、/" 这 64 个可打印字符进行编解码。
索引 | 对应字符 | 索引 | 对应字符 | 索引 | 对应字符 | 索引 | 对应字符 |
---|---|---|---|---|---|---|---|
0 | A | 17 | R | 34 | i | 51 | z |
1 | B | 18 | S | 35 | j | 52 | 0 |
2 | C | 19 | T | 36 | k | 53 | 1 |
3 | D | 20 | U | 37 | l | 54 | 2 |
4 | E | 21 | V | 38 | m | 55 | 3 |
5 | F | 22 | W | 39 | n | 56 | 4 |
6 | G | 23 | X | 40 | o | 57 | 5 |
7 | H | 24 | Y | 41 | p | 58 | 6 |
8 | I | 25 | Z | 42 | q | 59 | 7 |
9 | J | 26 | a | 43 | r | 60 | 8 |
10 | K | 27 | b | 44 | s | 61 | 9 |
11 | L | 28 | c | 45 | t | 62 | + |
12 | M | 29 | d | 46 | u | 63 | / |
13 | N | 30 | e | 47 | v | ||
14 | O | 31 | f | 48 | w | ||
15 | P | 32 | g | 49 | x | ||
16 | Q | 33 | h | 50 | y |
由于 64 = 2^6 ,最多需要 6 位二进制表示一个字符,在 ASCII 码中 8 位表示一个字符。为了节省字符空间,通常用 4 位 base64 编码来表示 3 位 ASCII 字符。 在内容结尾处可能存在凑不够 3 位 ASCII 字符的情况,base64 编码时使用 = 进行占位。
2.2 图像编解码
base64 编码:
const encode = (str) => {
var t = "", result = "";
var n, r, i, s, o, u, a;
var f = 0;
while (f < str.length) {
n = '0X' + str.slice(f, f += 2);
r = '0X' + str.slice(f, f += 2);
i = '0X' + str.slice(f, f += 2);
s = n >> 2;
o = (n & 3) << 4 | r >> 4;
u = (r & 15) << 2 | i >> 6;
a = i & 63;
result += _keyStr[s]
result += _keyStr[o]
result += _keyStr[u]
result += _keyStr[a]
}
return result
}
base64 解码代码:
const dict = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
const transform = (number) => {
let str = number.toString(16);
return str.length < 2 ? '0' + str : str;
}
const decode = (str) => {
var t = "", n, r, i, s, o, u, a, f = 0;
let result = ""
while (f < str.length) {
s = dict.indexOf(str.charAt(f++));
o = dict.indexOf(str.charAt(f++));
u = dict.indexOf(str.charAt(f++));
a = dict.indexOf(str.charAt(f++));
n = s << 2 | o >> 4;
r = (o & 15) << 4 | u >> 2;
i = (u & 3) << 6 | a;
result += transform(n)
result += transform(r)
result += transform(i)
}
return result
}
base64的图片被解码之后为图片原始数据,包含了图片头部信息和数据部分,我们可以给商品图片添加水印,在客户端本地进行图片合成等。