不使用 canvas 和 getImageData() 的唯一方法是将 PNG 文件加载为二进制类型的数组并“手动”在代码中解析文件。
先决条件:
- 为此,您需要 PNG 规范,您可以找到 here。
- 您需要知道如何使用类型化数组(为此,
DataView 是最合适的视图)。
- PNG 文件是基于 chunk 的,您需要知道如何解析块
典型的基于块的文件有一个称为 FourCC 标识符的 4 字节标头,后跟大小和杂项。数据取决于文件格式定义。
然后 chunks 被放置在这之后,通常包含一个 FOURCC(或四个字符代码),然后是没有块头的块的大小。原则上:
MAGIC FOURCC
尺寸/杂项 - 取决于定义
...
CHK1 - 块 FourCC
大小 - 无符号长
.... 数据
CHK2
尺寸
.... 数据
这种格式原则最初来自 80 年代中期的 Commodore Amiga 平台和 EA/IFF(交错文件格式)。
但在现代,一些供应商已经扩展或改变了块格式,因此对于 PNG 块,它实际上看起来像这样:
标头(总是 8 个字节和相同的字节值):
‰PNG(第一个字节是 0x89,原因见规范)
CR + 低频 0x0C0A
EOC + LF 0x1A0A
块:
SIZE (4 bytes, 可能为 0 (f.ex. IEND). Excl. chunk header and crc)
FOURCC(4 个字节,即“IHDR”、“IDAT”)
[...数据](长度:大小 x 字节)
CRC32(4个字节代表数据的CRC-32校验和)
(有关详细信息,请参阅上面引用的规范链接)。
而且 PNG 的字节顺序(字节序)始终是大字节序(“网络”顺序)。
这使得解析仅支持部分(或全部)块的文件变得容易。对于 PNG,您至少需要支持 (source):
-
IHDR 必须是第一个块;它包含(按此顺序)图像的宽度、高度、位深度和颜色类型。
-
IDAT 包含图像,可以在多个 IDAT 块之间拆分。这种拆分会稍微增加文件大小,但更容易流式传输 PNG。 IDAT 块包含实际的图像数据,即压缩算法的输出流。
-
IEND 标记文件结束。
如果您打算支持调色板(颜色索引)文件,您还需要支持 PLTE 块。解析 IHDR 块时,您将能够看到使用的颜色格式(RGB 数据类型为 2,RGBA 数据类型为 6,依此类推)。
解析本身很容易,因此您最大的挑战将是支持诸如 ICC 配置文件(当存在于 iCCP 块中时)之类的东西来调整图像颜色数据。一个典型的块是伽玛块 (gAMA),它包含一个伽玛值,您可以应用它来将数据转换为线性格式,以便在应用显示伽玛时正确显示(还有其他与颜色相关的特殊块)。
第二大挑战是使用 INFLATE 的解压。您可以使用诸如 PAKO zlib port 之类的项目来为您完成这项工作,并且此端口的性能接近原生 zlib。除此之外,如果要对数据进行错误检查(推荐),还应该支持 CRC-32 检查。
出于安全原因,您应该始终检查字段是否包含它们应该包含的数据以及保留空间是否使用 0 或定义的数据进行初始化。
希望这会有所帮助!
块解析器示例:(注意:不会在 IE 中运行)。
function pngParser(buffer) {
var view = new DataView(buffer),
len = buffer.byteLength,
magic1, magic2,
chunks = [],
size, fourCC, crc, offset,
pos = 0; // current offset in buffer ("file")
// check header
magic1 = view.getUint32(pos); pos += 4;
magic2 = view.getUint32(pos); pos += 4;
if (magic1 === 0x89504E47 && magic2 === 0x0D0A1A0A) {
// parse chunks
while (pos < len) {
// chunk header
size = view.getUint32(pos);
fourCC = getFourCC(view.getUint32(pos + 4));
// data offset
offset = pos + 8;
pos = offset + size;
// crc
crc = view.getUint32(pos);
pos += 4;
// store chunk
chunks.push({
fourCC: fourCC,
size: size,
offset: offset,
crc: crc
})
}
return {chunks: chunks}
}
else {
return {error: "Not a PNG file."}
}
function getFourCC(int) {
var c = String.fromCharCode;
return c(int >>> 24) + c(int >>> 16 & 0xff) + c(int >>> 8 & 0xff) + c(int & 0xff);
}
}
// USAGE: ------------------------------------------------
fetch("//i.imgur.com/GP6Q3v8.png")
.then(function(resp) {return resp.arrayBuffer()}).then(function(buffer) {
var info = pngParser(buffer);
// parse each chunk here...
for (var i = 0, chunks = info.chunks, chunk; chunk = chunks[i++];) {
out("CHUNK : " + chunk.fourCC);
out("SIZE : " + chunk.size + " bytes");
out("OFFSET: " + chunk.offset + " bytes");
out("CRC : 0x" + (chunk.crc>>>0).toString(16).toUpperCase());
out("-------------------------------");
}
function out(txt) {document.getElementById("out").innerHTML += txt + "<br>"}
});
body {font: 14px monospace}
<pre id="out"></pre>
从这里您可以提取 IHDR 以查找图像大小和颜色类型,然后使用 IDAT 块来缩小(PNG 使用每个扫描线的过滤器,这确实使事情变得有点复杂,以及隔行扫描模式,请参阅规范)和你几乎完成了;)