自己随手写的一个简单的png图片类,记录一下过程,欢迎提问和建议
我们require什么?
我们甚至不需要node_modules文件夹
const zlib = require('zlib'); // 我们需要对png数据块进行deflate压缩
const fs = require('fs'); // 输出图片到文件
const crc = require('./crc'); // 计算一个buffer的crc校验码,你可以用任何自己喜欢的实现方式
你不需要详细了解crc校验的细节,感兴趣的话可以自行百度,这个文件我会附在后面
png文件的构成
png文件是由一个个数据块组成的,每个数据块包含4或3个部分: LENGTH: 这个数据块chunk data部分的长度(字节),32位无符号整数(4字节) CHUNK TYPE: 这个数据块的类型,固定4字节(4个英文字符) CHUNK DATA: 数据块的内容,根据chunk type决定,长度不固定,LENGTH=0时不存在该部分 CRC: 对CHUNK TYPE+CHUNK DATA进行CRC计算生成的校验码(4字节)
png的头部——0x89+'PNG'+0x0d0a1a0a
所有png文件的开头都有固定的8字节,用来让读取它的程序知道这是个png文件:
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
0x50,0x4e,0x47=‘PNG’
第一个数据块——IHDR
png文件的第一个数据块,固定13字节信息,包含了png文件的基本信息 width 4字节 图片宽度 height 4字节 图片高度 Bit depth 1字节 图像位深,这里我们固定设置为8(RGBA每通道各占8位(0~255)) Colour type 1字节 颜色类型,这里我们固定设置为6(RGBA带透明度的彩图) Compression method 1字节 不用管 Filter method 1字节 不用管 Interlace method 1字节 不用管
IHDR在一开始就已经确定了全部内容,所以写一个生成IHDR Buffer的函数吧
function ihdr(width = 0,
height = 0,
bitDepth = 8,
colourType = 6,
compressionMethod = 0,
filterMethod = 0,
interlaceMethod = 0) {
// 写入数据块内容
const buf = Buffer.allocUnsafe(17);
buf.write('IHDR', 0); // CHUNK TYPE 方便起见这里直接和内容写在同一个buffer里了
// 写 CHUNK DATA
buf.writeUInt32BE(width, 4);
buf.writeUInt32BE(height, 8);
buf.writeUInt8(bitDepth, 12);
buf.writeUInt8(colourType, 13);
buf.writeUInt8(compressionMethod, 14);
buf.writeUInt8(filterMethod, 15);
buf.writeUInt8(interlaceMethod, 16);
// crc 由数据块名+数据块内容进行crc计算获得
const ihdrCrc = Buffer.allocUnsafe(4);
ihdrCrc.writeInt32BE(crc(buf));
// png数据块结构:4字节数据块内容长度,4字节数据块名,数据块内容,4字节crc
return Buffer.concat([Buffer.from([0, 0, 0, 13]), buf, ihdrCrc]);
}
在继续之前我们先写一个类吧
class PNG {
constructor(width, height) {
this.width = width;
this.height = height;
this.ihdr = ihdr(width, height);
}
}
另一个数据块——IDAT
这是用来储存图像内容的数据块,结构很简单
LENGTH:我们现在暂时不知道,留着
CHUNK TYPE:Buffer.from('IDAT')简单粗暴
CHUNK DATA:我们设置的是每通道8位(每像素32位=4字节)的图片,所以要申请的内存大小:宽*高*每像素4字节+图像高度
在png图片中,每一行像素开头会有1字节的行标志位0x00,所以我们需要额外的height字节
idat数据块应包含的是被deflate压缩后的数据,所以我们现在只能先初始化一下这个数据块,留到最后再处理:
constructor(width, height) {
// ...
// 填充255,则默认是一个纯白的图片
this.idat = Buffer.alloc((width * height * 4) + height, 255);
}
CRC:虽然已经有了CHUNK TYPE和CHUNK DATA,但因为CHUNK DATA还需进行压缩所以暂时无法计算CRC
写一个处理idat数据块的函数:
getIdat() {
// 数据块名
const idatName = Buffer.from('IDAT');
// idat数据块内容需要进行一次deflate压缩
const idatData = zlib.deflateSync(this.idat);
// 长度
const idatLen = Buffer.alloc(4);
idatLen.writeUInt32BE(this.idat.byteLength);
// crc
const idatCrc = Buffer.allocUnsafe(4);
idatCrc.writeInt32BE(crc(Buffer.concat([idatName, this.idat])));
return Buffer.concat([idatLen, idatName, idatData, idatCrc]);
}
结束标志
简单粗暴:Buffer.from('IEND')
输出到png文件
简单的按顺序合并一下各数据块就可以写到文件里了
writeFile(path) {
const idat = this.getIdat();
// 合并数据块为png文件buffer
const buf = Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), // 固定的png头
this.ihdr, // ihdr数据块
idat, // idat数据块
Buffer.from('IEND'), // 固定的png尾
]);
fs.writeFileSync(path, buf);
}
然后我们可以立刻试用一下了:
const p = new PNG(30, 30);
p.writeFile('./png.png');
这样就能生成一个30*30的纯白色图片了
设置任意像素的颜色
目前我们已经可以生成指定大小的纯白色png图片了,下面我们写一个函数来设置图片的内容,很简单
setPixel(x, y, pixel) {
// 计算指定像素在idat buffer中的实际位置
const pos = (this.width * y * 4) + y + 1 + (x * 4);
this.idat.writeUInt32BE(pixel, pos);
}
那么一个有基本功能的png类就完成了
const p = new PNG(30, 30);
for(let i = 0; i < 30; i++) {
png.setPixel(i, 15, 0xFF0000FF); // 红 绿 蓝 不透明度
}
p.writeFile('./redLine.png');
生成了一张中间有一条红线的PNG图片
上面的图片在很多浏览器中可能无法正常显示,因为填充buffer时把本应是0x00的行标志位填充为了0xff,少数图片解码器可以自动修复此错误
感兴趣的话可以自己继续实现画线,画其他图形的功能
引用资料
W3C PNG标准https://www.w3.org/TR/2003/REC-PNG-20031110/
crc.js
index.js
https://www.npmjs.com/package/canvas
不错,加油