【问题标题】:Javascript (or pseudocode) to properly append two PNGs using arraybuffers/canvasJavascript(或伪代码)使用 arraybuffer/canvas 正确附加两个 PNG
【发布时间】:2016-10-20 14:55:38
【问题描述】:

我有两张图片(合并后的宽高比为 1:1)。如果我在 unix 上使用 convert a.png b.png -append c.png 将它们组合起来,它会完美运行。我试图在javascript中实现这一点。我将数组缓冲区(包含 img 数据)添加在一起,因为在画布中绘制它们似乎不会产生相同的图像。如果我只是附加每个arraybuffer,图像比例是2:1;有人知道如何正确附加数组缓冲区,类似于convert 所做的吗?

编辑:详细地说,简单地堆叠在画布上不会工作(我已经尝试过)。这可能是由于低级画布代码,我怀疑这是由于画布如何在两个图像之间的边界处连接像素。它需要是数组缓冲区。

【问题讨论】:

  • 如何填充arraybuffer/XHR?
  • 我已经从 ajax get 请求中加载了它们:数据是正确的并且存在,但是简单地附加两个缓冲区会创建垂直合并:我需要水平

标签: javascript image-processing canvas png


【解决方案1】:

如果您出于某种原因不想通过Image 加载图像,那么唯一的选择是手动解析和解压缩文件。由于 ICC/gamma 支持,浏览器确实可以更改图像。这不会发生在画布步骤中,而是在图像加载和转换为 RGBA 数据期间。

话虽如此,getImageDate()/putImageData() 期间的过程也可能由于(未)预乘和舍入误差而改变像素值。

使用画布将两个PNG图像合并为一个的示例:

var ctx = c.getContext("2d"),                   // canvas 2d context
    img1 = new Image,                           // create two image
    img2 = new Image,                           // elements
    count = 2;                                  // Track for loader

// load images
img1.onload = img2.onload = function() {        // make sure images are
  if (!--count) append();                       // loaded first
};
img1.crossOrigin = img2.crossOrigin = "";       // need this for this demo
img1.src = "http://i.imgur.com/hlHEhUhb.jpg";   // random images...
img2.src = "http://i.imgur.com/ynzkv40b.jpg";

// process images
function append() {

  // use width to sum the images
  c.width = img1.width + img2.width;            // set total height 
  c.height = Math.max(img1.height,img2.height); // set max height
  
  ctx.drawImage(img1, 0, 0);                    // draw in image 1
  ctx.drawImage(img2, img1.width, 0) ;          // draw in image 2
  
  console.log(c.toDataURL());                   // extract, send to server
}
<canvas id=c></canvas>

您不能简单地将 PNG 数据相互合并,而不先对它们进行解码。这是因为图像数据块被压缩(压缩),并且 PNG 文件中的每个扫描线都使用一个初始字节来描述正在使用的线过滤器。

如果只是垂直合并它们可能会使缩小的数据无效,并且由于将引入额外的过滤字节,它会使行数据与长度无效。每行的过滤器也可能不同。

因此无法解析、解压缩和解码源 PNG 文件。但要解析 PNG 文件,您必须知道文件格式是如何构建的。

PNG 文件格式

PNG文件中的主要文件结构是:

-Signature-     8 bytes
IHDR chunk      required (width, height, depth, mode etc.)
[PLTE chunk]    required for indexed color mode
[Misc chunks]   optional ancillary and private chunks
IDAT chunk      required, can be multiple
IEND chunk      required, last chunk (data-less)

在这种情况下,可以忽略任何其他块,除非您使用索引调色板,在这种情况下,您还需要考虑 PLTE 块。

如果当前块未知或不需要,块允许您跳到下一个块。块由 8 个字节构成,后跟数据,然后是 4 个字节的 CRC-32 校验和(不需​​要数据,就像 IEND 块一样):

0x00 SIZE     (4 bytes)
0x04 FOURCC   (4 bytes)
0x08 DATA     (variable, can be 0)
0x?? CRC-32   (4 bytes)

大小仅代表数据。该名称将是块名称的 ASCII 表示,始终为四个字节(“IDAT”、“IEND”、...)。

如果您不希望验证数据,可以忽略 CRC-32 校验和,但当您生成新的 PNG 文件时不能忽略,因为大多数 PNG 查看器/解析器使用此值并且它包含块名称。

所有值都以大端字节序无符号。

阅读块

读取分块数据文件(如 PNG)的典型方法是在第一个块处初始化起始偏移量。然后在读取和移动文件光标的同时进行迭代,检查块名。

例如:

var pos = 8;                          // first chunk position
var dv = new DataView(arraybuffer);   // use a DataView

制作一些辅助函数来读取和移动位置:

function getUint32() {                // and for Uint16 etc.
  var data = dv.getUint32(pos);       // use big-endian byte-order
  pos += 4;
  return data
}

// decode chunk name to string (from pngtoy)
function getFourCC() {
    var v = getUint32(),
        c = String.fromCharCode;
    return  c((v & 0xff000000)>>>24) + c((v & 0xff0000)>>>16) + 
            c((v & 0xff00)>>>8) + c((v & 0xff)>>>0)
}

现在我们可以按预期使用文件缓冲区:

// repeated actions:
var size = getUint32();
var name = getFourCC();
var data, crc;

if (name === "IHDR") {                          // check chunk type
  data = new Uint8Array(dv.buffer, pos, size);  // get data section from chunk
  pos += size;                                  // next chunk or the end
  crc = getUint32();                            // read CRC-32 checksum
  // validate CRC-32 here
}
else pos += size + 4;                           // skip data and crc

提示:即使块被跳过,也可能是根据 CRC 校验和验证数据以发现文件损坏的早期迹象。

IDAT 块总是 包含压缩数据,因为这是格式规范中唯一有效的存储形式,必须先膨胀。对于这个过程,我会(一如既往)推荐Pako implementation of the zlib library。

读取过程

然后每个输入图像的读取过程变为(需要使用DataView):

  • 检查魔术头/签名。共有 8 个字节,应始终为以下序列:
    0x89504E47 0x0D0A1A0A(大端)。
  • 如果正常,第一个块 (IHDR) 将在文件的第 8 位找到。您需要解析此标头的内容以查找宽度、高度以及位图深度(16、8、4、1)和类型(RGB、RGBA、灰度、位图等)以及图像是否是隔行扫描或不是。
  • 获得这些数据后,您可以扫描 IDAT 块。注意复数 - 通常只有一个 IDAT 块,但拥有多个 IDAT 块是完全有效的。当您到达 IEND 块时,就没有更多数据了。当位图被拆分为多个 IDAT 块时,有效的 PNG 文件在 IDAT 和 IEND 块之间不会有任何其他块。
  • 通过 inflate 传递数据以解压缩它。 (提示:使用 Inflate 实例而不是静态函数可以获取每个单独的 IDAT 块数据,然后解压缩到单个缓冲区。
  • 现在您将拥有一个原始但未过滤 PNG 位图

我们仍然无法合并文件,因为我们需要使用过滤字节解码每个扫描线。 PNG 中有五种不同的线路过滤器,其中 0 表示不需要过滤,直至更复杂的 4 Paeth 过滤器。

此外,图像可以是隔行扫描 (Adam-7),由于是逐行扫描,因此需要不同的方法。

当您对每条扫描线进行解码(并在需要时进行去隔行扫描)后,您将获得一个不受浏览器中 ICC/gamma 影响的原始位图。

需要采取额外的步骤来检查两个图像是否属于同一类型(例如 RGB、RGBA 等)。如果不是,则必须另外将一个图像转换为另一种格式,通常通过“升级”信息/质量较少的一种。如果格式和深度相同,你应该很高兴。

如果大小不同,最终结果会留下某种间隙,可能需要填充来填充没有覆盖的空白像素,具体取决于格式,如果不想要透明度等内容等等

现在您可以水平或垂直合并两个位图。

合并两个位图

你提到你想水平合并位图 -

  • 设置一个新缓冲区,其大小为图像 1 宽度 + 图像 2 宽度乘以单个像素大小(RGB 为 3,RGBA 为 4 等)
  • 将高度定义为两个高度中的最大值
  • 确定您是否需要/想要使用填充/零填充(高度 1 !== 高度 2)

为新缓冲区设置一个主循环,然后在每个扫描线的图像 1 和 2 之间交替,以便将前两个扫描线作为一个单独的扫描线复制到新缓冲区中。

编写过程

然后再次保存图像的相反过程是:

  • 设置签名
  • 添加 IHDR 块并使用新的大小、格式和深度进行更新
  • 添加 IDAT 块
    • 对每条扫描线进行编码(为简单起见,您可以使用过滤器 0,但它会增加大小)
    • 使用 zlib 压缩数据并添加。使用压缩后的大小更新块的大小
    • Calculate CRC-32 checksums
  • 添加 IEND 块

我的策略是使用普通 Array 分部分构建文件,以保存每个类型化的数组部分(签名)和块 + 数据。然后将数组传递给 Blob,Blob 会将这些部分连接到单个二进制缓冲区。

例如:

var arr = [];
arr.push(taSig);   // ta* = typed array
arr.push(taIHDR);
arr.push(taIDAT);
arr.push(taIEND);

然后将数组传入一个Blob:

var blob = new Blob(arr, {type: "image/png"});

完整的PNG文件格式规范可见here。

我建议您查看我的 pngtoy(PNG 解析器和解码器,MIT 许可)了解详细信息。它执行与上述类似的步骤来获取原始解码位图。

【讨论】:

  • 出色而全面的答案。这真的是我所追求的:解码的 png 字节数组的格式。如果可行,我将对此进行测试并为您提供最佳答案。
  • 更新了更多细节。
  • 事实证明,重新处理我的图像数据库以拆分文件开始(以节省客户端任何处理延迟时间)更容易 - 话虽如此,这就是我正在寻找的问题的答案为当时(并且将来可能需要!)干杯。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多