如何使用 javascript 从 PNG 中提取像素信息(getImageData 替代方案)

How to extract pixel information from PNG using javascript (getImageData alternatives)

我正在尝试从 PNG 图像中获取像素数据进行处理。当前的方法是使用 canvas.drawImage 后跟 canvas.getImageData (example here)。我正在寻找替代品。

当前方法的问题是浏览器修改受 alpha 影响的像素值,如 here and here.

所述

有人问过这个问题before,但没有满意的答案。

不使用 canvas 和 getImageData() 的唯一方法是将 PNG 文件加载为二进制类型数组并在代码 "manually".[=27 中解析文件=]

先决条件:

  • 为此,您需要 PNG 规范,您可以找到 here.
  • 您需要知道如何使用类型化数组(为此,DataView 是最合适的视图)。
  • PNG 文件是基于 chunk 的,您需要知道如何解析 chunk

典型的基于块的文件有一个四字节 header 称为 FourCC 标识符,后跟大小和杂项。数据取决于文件格式定义。

然后 chunks 被放置在此之后,通常包含一个 FOURCC(或四个字符代码),然后是没有块 header 的块的大小。原则上:

    MAGIC FOURCC
    SIZE/MISC    - depending on definition
    ...

    CHK1         - Chunk FourCC
    SIZE         - unsigned long
    .... data

    CHK2
    SIZE
    .... data

此格式原则最初来自 Commodore Amiga 平台和 EA/IFF(交错文件格式),可追溯到 80 年代中期。

但在现代,一些供应商已经扩展或改变了块格式,因此对于 PNG 块,它实际上看起来像这样:

Header(总是 8 个字节和相同的字节值):

‰PNG       (first byte is 0x89, see specs for reason)
CR + LF    0x0C0A
EOC + LF   0x1A0A

块:

SIZE      (4 bytes, may be 0 (f.ex. IEND). Excl. chunk header and crc)
FOURCC    (4 bytes, ie. "IHDR", "IDAT")
[...data] (length: SIZE x bytes)
CRC32     (4 bytes representing the CRC-32 checksum of the data)

(有关详细信息,请参阅上面的参考规范 link)。

PNG 的 byte-order(字节顺序)始终是 big-endian("network" 顺序)。

这使得解析仅支持部分(或全部)块的文件变得容易。对于 PNG,您至少需要支持 (source):

  • IHDR 必须是第一个块;它包含(按此顺序)图像的宽度、高度、位深度和颜色类型。
  • IDAT 包含图像,可以在多个 IDAT 块之间拆分。这种拆分会稍微增加文件大小,但可以更轻松地流式传输 PNG。 IDAT 块包含实际图像数据,它是压缩算法的输出流。
  • IEND 标记文件结束。

如果您打算支持调色板(颜色索引)文件,您还需要支持 PLTE 块。解析 IHDR 块时,您将能够看到使用的颜色格式(RGB 数据类型为 2,RGBA 数据类型为 6,依此类推)。

解析本身很容易,因此您最大的挑战是支持 ICC 配置文件(当出现在 iCCP 块中时)之类的东西来调整图像颜色数据。一个典型的块是伽玛块(gAMA),它包含一个伽玛值,您可以应用它来将数据转换为线性格式,以便在应用显示伽玛时正确显示(还有其他与颜色相关的特殊块).

第二大挑战是使用 INFLATE 的解压。您可以使用 PAKO zlib port 等项目为您完成这项工作,并且此端口的性能接近原生 zlib。除此之外,如果你想对数据进行错误检查(推荐),还应该支持CRC-32检查。

出于安全原因,您应该始终检查字段是否包含它们应该包含的数据以及保留的 space 是否使用 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 使用每个扫描线的过滤器,这会使事情变得有点复杂,以及隔行扫描模式,请参阅规格) 就快完成了 ;)