png的故事_ 获取图片信息和像素内容.

网站前端开发_前端开发者web前端开发

https://www.rokub.com

png的故事: 获取图片信息和像素内容前言现在时富媒体时代, 图片的重要性对于数十亿互联网用户来说不言而喻, 图片本身就是像素点阵的合集, 但是为了如何更快更好的存储图片而诞生了各种各样的图片格式: jpeg、 png、 gif、 webp等, 而这次我们要拿来开刀的, 就是png。 简介首先, png是什么鬼? 我们来看看wiki上的一句话简介: Portable Network Graphics(PNG) is a raster graphics file format that supports lossless data compression.也就是说, png是一种使用无损压缩的图片格式, 而大家熟知的另外一种图片格式—— jpeg则是采用有损压缩的方式。 用通俗易懂的方式来讲, 当原图片数据被编码成png格式后, 是可以完全还原成原本的图片数据的, 而编码成jpeg则会损耗一部分图片数据, 这是因为两者的编码方式和定位不同。 jpeg着重于人眼的观感, 保留更多的亮度信息, 去掉一些不影响观感的色度信息, 因此是有损耗的压缩。 png则保留原始所有的颜色信息, 并且支持透明/ alpha通道, 然后采用无损压缩进行编码。 因此对于jpeg来说, 通常适合颜色更丰富、 可以在人眼识别不了的情况下尽可能去掉冗余颜色数据的图片, 比如照片之类的图片; 而png适合需要保留原始图片信息、 需要支持透明度的图片。 以下, 我们来尝试获取png编码的图片数据: 结构图片是属于2进制文件, 因此在拿到png图片并想对其进行解析的话, 就得以二进制的方式进行读取操作。 png图片包含两部分: 文件头和数据块。 文件头png的文件头就是png图片的前8个字节, 其值为[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], 人们常常把这个头称之为“ 魔数”。 玩过linux的同学估计知道, 可以使用 file 命令类判断一个文件是属于格式类型, 就算我们把这个文件类型的后缀改得乱七八糟也可以识别出来, 用的就是判断“ 魔数” 这个方法。 有兴趣的同学还可以使用 String.fromCharCode 将这个“ 魔数” 转成字符串看看, 就知道为什么png会取这个值作为文件头了。 用代码来判断也很简单: 读取指定长度字节

function readBytes(buffer, begin, length) {
returnArray.prototype.slice.call(buffer,begin,begin+length);
}
let header = readBytes(pngBuffer, 0, 8);
[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] 1 2 3 4 5 6 读取指定长度字节
function readBytes(buffer, begin, length) {
returnArray.prototype.slice.call(buffer,begin,begin+length);
}
let header = readBytes(pngBuffer, 0, 8);
[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] 数据块去掉了png图片等前8个字节, 剩下的就是存放png数据的数据块, 我们通常称之为 chunk。 顾名思义, 数据块就是一段数据, 我们按照一定规则对png图片( 这里指的是去掉了头的png图片数据, 下同) 进行切分, 其中一段数据就是一个数据块。 每个数据块的长度是不定的, 我们需要通过一定的方法去提取出来, 不过我们要先知道有哪些类型的数据块才好判断。 数据块类型数据块类型有很多种, 但是其中大部分我们都不需要用到, 因为里面没有存储我们需要用到的数据。 我们需要关注的数据块只有以下四种: IHDR: 存放图片信息。 PLTE: 存放索引颜色。 IDAT: 存放图片数据。 IEND: 图片数据结束标志。 只要解析这四种数据块就可以获取图片本身的所有数据, 因此我们也称这四种数据块为“ 关键数据块”。 数据块格式数据块格式如下: 描述 长度 数据块内容长度 4 字节 数据块类型 4 字节 数据块内容 不定字节 crc冗余校验码 4 字节这样我们就可以轻易的指导当前数据块的长度了, 即 数据块内容长度 + 12 字节, 用代码实现如下: 读取32位无符号整型数
function readInt32(buffer, offset) {
offset=offset||0;
return (buffer[offset] <<24) + (buffer[offset+1] <<16) + (buffer[offset+2] <<8) + (buffer[offset+3] <<0);
}
let length = readInt32(readBytes(4));
数据块内容长度
let type = readBytes(4);
数据块类型
let chunkData = readBytes(length);
数据块内容
let crc = readBytes(4);
crc冗余校验码 1 2 3 4 5 6 7 8 9 10 读取32位无符号整型数
function readInt32(buffer, offset) {
offset=offset||0;
return (buffer[offset] <<24) + (buffer[offset+1] <<16) + (buffer[offset+2] <<8) + (buffer[offset+3] <<0);
}
let length = readInt32(readBytes(4));
数据块内容长度
let type = readBytes(4);
数据块类型
let chunkData = readBytes(length);
数据块内容
let crc = readBytes(4);
crc冗余校验码这里的crc冗余校验码在我们解码过程中用不到, 所以这里不做详解。 除此之外, 数据块内容长度和数据块内容好解释, 不过数据块类型有何作用呢, 这里我们先将这个 type 转成字符串类型: 将buffer数组转为字符串
function bufferToString(buffer) {
letstr=”;
for (leti=0,len=buffer.length;i<len;i++) {
str+=String.fromCharCode(buffer[i]);
}
returnstr;
}
type = bufferToString(type);
1 2 3 4 5 6 7 8 9 10 将buffer数组转为字符串
function bufferToString(buffer) {
letstr=”;
for (leti=0,len=buffer.length;i<len;i++) {
str+=String.fromCharCode(buffer[i]);
}
returnstr;
}
type = bufferToString(type);
然后会发现type的值是四个大写英文字母, 没错, 这就是上面提到的数据块类型。 上面还提到了我们只需要解析关键数据块, 因此遇到 type 不等于IHDR、 PLTE、 IDAT、 IEND中任意一个的数据块就直接舍弃好了。 当我们拿到一个关键数据块, 就直接解析其数据块内容就可以了, 即上面代码中的 chunkData 字段。 IHDR类型为IHDR的数据块用来存放图片信息, 其长度为固定的13个字节: 描述 长度 图片宽度 4 字节 图片高度 4 字节 图像深度 1 字节 颜色类型 1 字节 压缩方法 1 字节 过滤方式 1 字节 扫描方式 1 字节其中宽高很好解释, 直接转成32位整数, 就是这张png图片等宽高( 以像素为单位)。 压缩方法目前只支持一种( deflate / inflate 压缩算法), 其值为0; 过滤方式也只有一种( 包含标准的5种过滤类型), 其值为0; 扫描方式有两种, 一种是逐行扫描, 值为0, 还有一种是Adam7隔行扫描, 其值为1, 此次只针对普通的逐行扫描方式进行解析, 因此暂时不考虑Adam7隔行扫描。 图片深度是指每个像素点中的每个通道( channel) 占用的位数, 只有1、 2、 4、 8 和16这5个值; 颜色类型用来判断每个像素点中有多少个通道, 只有0、 2、 3、 4 和6这5个值: 颜色类型的值 占用通道数 描述 0 1 灰度图像, 只有1个灰色通道 2 3 rgb真彩色图像, 有RGB3色通道 3 1 索引颜色图像, 只有索引值一个通道 4 2 灰度图像 + alpha通道PLTE类型为PLTE的数据块用来存放索引颜色, 我们又称之为“ 调色板”。 由IHDR数据块解析出来的图像信息可知, 图像的数据可能是以索引值的方式进行存储。 当图片数据采用索引值的时候, 调色板就起作用了。 调色板的长度和图像深度有关, 假设图像深度的值是x, 则其长度通常为 2 的x次幂 * 3。 原因是图像深度保存的就是通道占用的位数, 而在使用索引颜色的时候, 通道里存放的就是索引值, 2 点x次幂就表示这个通道可能存放的索引值有多少个, 即调色板里的颜色数。 而每个索引颜色是RGB3色通道存放的, 因此此处还需要乘以3。 通常使用索引颜色的情况下, 图像深度的值即为8, 因而调色板里存放的颜色就只有256种颜色, 长度为 256 * 3 个字节。 再加上1位布尔值表示透明像素, 这就是我们常说的png8图片了。 IDAT类型为IDAT的数据块用来存放图像数据, 跟其他关键数据块不同的是, 其数量可以是连续的复数个; 其他关键数据块在1个png文件里有且只有1个。 这里的数据得按顺序把所有连续的IDAT数据块全部解析并将数据联合起来才能进行最终处理, 这里先略过。
let dataChunks = [];
let length = 0;
总数据长度…
while ( /* 存在IDAT数据块 */ ) {
dataChunks.push(chunkData);
length+=chunkData.length;
}
1 2 3 4 5 6 7 8 9
let dataChunks = [];
let length = 0;
总数据长度…
while ( /* 存在IDAT数据块 */ ) {
dataChunks.push(chunkData);
length+=chunkData.length;
}
IEND当解析到类型为IEND的数据块时, 就表明所有的IDAT数据块已经解析完毕, 我们就可以停止解析了。 IEND整个数据块的值时固定的:[0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82], 因为IEND数据块没有数据块内容, 所以其数据块内容长度字段( 数据块前4个字节) 的值也是0。 解析解压缩当我们收集完IDAT的所有数据块内容时, 我们要先对其进行解压缩:
const zlib = require(‘zlib’);
let data = new Buffer(length);
let index = 0;
dataChunks.forEach((chunkData) => {
chunkData.forEach((item)=>{
data[index++] =item
});
});
inflate解压缩 data = zlib.inflateSync(new Buffer(data));
1 2 3 4 5 6 7 8 9 10
const zlib = require(‘zlib’);
let data = new Buffer(length);
let index = 0;
dataChunks.forEach((chunkData) = > {
chunkData.forEach((item) = > {
data[index++] = item
});
});
inflate解压缩 data = zlib.inflateSync(new Buffer(data));
扫描上面说过, 此次我们只考虑逐行扫描的方式: 读取8位无符号整型数
function readInt8(buffer, offset) {
offset = offset || 0;
return buffer[offset] << 0;
}
let width;
解析IHDR数据块时得到的图像宽度
let height;
解析IHDR数据块时得到的图像高度
let colors;
解析IHDR数据块时得到的通道数
let bitDepth;
解析IHDR数据块时得到的图像深度
let bytesPerPixel = Math.max(1, colors * bitDepth / 8);
每像素字节数
let bytesPerRow = bytesPerPixel * width;
每行字节数
let pixelsBuffer = new Buffer(bytesPerPixel * width * height);
存储过滤后的像素数据
let offset = 0;
当前行的偏移位置 逐行扫描解析
for (let i = 0, len = data.length; i < len; i += bytesPerRow + 1) {
letscanline=Array.prototype.slice.call(data,i+1,i+1+bytesPerRow);
当前行
letargs= [scanline,bytesPerPixel,bytesPerRow,offset];
第一个字节代表过滤类型
switch(readInt8(data,i)) {
case0:
filterNone(args);
break;
case1:
filterSub(args);
break;
case2:
filterUp(args);
break;
case3:
filterAverage(args);
break;
case4:
filterPaeth(args);
break;
default:
thrownewError(‘未知过滤类型!’);
}
offset += bytesPerRow;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 读取8位无符号整型数
function readInt8(buffer, offset) {
offset=offset||0;
returnbuffer[offset] <<0;
}
let width;
解析IHDR数据块时得到的图像宽度
let height;
解析IHDR数据块时得到的图像高度
let colors;
解析IHDR数据块时得到的通道数
let bitDepth;
解析IHDR数据块时得到的图像深度
let bytesPerPixel = Math.max(1, colors * bitDepth / 8);
每像素字节数
let bytesPerRow = bytesPerPixel * width;
每行字节数
let pixelsBuffer = new Buffer(bytesPerPixel * width * height);
存储过滤后的像素数据
let offset = 0;
当前行的偏移位置 逐行扫描解析
for (let i = 0, len = data.length; i < len; i += bytesPerRow + 1) {
letscanline=Array.prototype.slice.call(data,i+1,i+1+bytesPerRow);
当前行
letargs= [scanline,bytesPerPixel,bytesPerRow,offset];
第一个字节代表过滤类型
switch(readInt8(data,i)) {
case0:
filterNone(args);
break;
case1:
filterSub(args);
break;
case2:
filterUp(args);
break;
case3:
filterAverage(args);
break;
case4:
filterPaeth(args);
break;
default:
thrownewError(‘未知过滤类型!’);
}
offset += bytesPerRow;
}
上面代码前半部分不难理解, 就是通过之前解析得到的图像宽高, 再加上图像深度和通道数计算得出每个像素占用的字节数和每一行数据占用的字节数。 因此我们就可以拆分出每一行的数据和每一个像素的数据。 在得到每一行数据后, 就要进行这个png编码里最关键的1步—— 过滤。 过滤早先我们说过过滤方法只有1种, 其中包含5种过滤类型, 图像每一行数据里的第一个字节就表示当前行数什么过滤类型。 png为什么要对图像数据进行过滤呢?
大多数情况下, 图像的相邻像素点的色值时很相近的, 而且很容易呈现线性变化( 相邻数据的值是相似或有某种规律变化的), 因此借由这个特性对图像的数据进行一定程度的压缩。 针对这种情况我们常常使用一种叫差分编码的编码方式, 即是记录当前数据和某个标准值的差距来存储当前数据。
比如说有这么一个数组[99, 100, 100, 102, 103], 我们可以将其转存为[99, 1, 0, 2, 1]。 转存的规则就是以数组第1位为标准值, 标准值存储原始数据, 后续均存储以前1位数据的差值。 当我们使用了差分编码后, 再进行deflate压缩的话, 效果会更好( deflate压缩是LZ77延伸出来的一种算法, 压缩频繁重复出现的数据段的效果是相当不错的, 有兴趣的同学可自行去了解)。

网站前端开发_前端开发者web前端开发

https://www.rokub.com

» 本文来自:前端开发者 » 《png的故事_ 获取图片信息和像素内容.》
» 本文链接地址:https://www.rokub.com/2379.html
» 您也可以订阅本站:https://www.rokub.com
赞(0)
64K

评论 抢沙发

评论前必须登录!