Javascript(或伪代码)使用arraybuffers/canvas正确附加两个PNG

use*_*954 0 javascript png canvas image-processing

我有两个图像(组合时将具有1:1的宽度:高度比).如果我convert a.png b.png -append c.png在unix上使用它们,它可以很好地工作.我试图在javascript中实现这一点.我正在将arraybuffers(包含img数据)添加到一起,因为在画布中绘制它们似乎都不会产生相同的图像.如果我只是附加每个数组缓冲区,则图像比例为2:1; 有谁知道如何正确附加数组缓冲区,类似于什么convert

编辑:详细说明,只是在画布上堆叠将无法正常工作(我已经尝试过).这可能是由于低级别的画布代码,我怀疑这是由于画布如何连接两个图像之间的边界处的像素.它需要是arraybuffers.

小智 7

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

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

使用canvas将两个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
}
Run Code Online (Sandbox Code Playgroud)
<canvas id=c></canvas>
Run Code Online (Sandbox Code Playgroud)

您不能简单地将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)
Run Code Online (Sandbox Code Playgroud)

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

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

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

大小仅代表数据.名称将是块名称的ASCII表示,总是四个字节("IDAT","IEND",...).

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

所有值都以big-endian字节顺序无符号.

读大块

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

例如:

var pos = 8;                          // first chunk position
var dv = new DataView(arraybuffer);   // use a DataView
Run Code Online (Sandbox Code Playgroud)

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

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)
}
Run Code Online (Sandbox Code Playgroud)

现在允许我们按预期使用文件缓冲区:

// 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
Run Code Online (Sandbox Code Playgroud)

提示:即使跳过了块,它也可能是根据CRC校验和验证数据以找到文件损坏的早期指示的一点.

IDAT块总是包含缩小的数据,因为这是格式规范中唯一有效的存储形式,必须先进行充气.对于这个过程,我建议(一如既往)zlib库Pako实现.

阅读过程

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

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

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

此外,图像可以交错(Adam-7),由于是渐进的,因此需要不同的方法.

当您解码每条扫描线(并在需要时进行反交错)时,您的原始位图不受浏览器的ICC/gamma影响.

需要采取额外的步骤来检查两个图像是否属于同一类型(例如RGB,RGBA等).如果不是,那么另外还需要通过"升级"少量图像来转换为其他格式.信息/质量.如果相同的格式和深度你应该好.

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

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

合并两个位图

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

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

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

写作过程

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

  • 设置签名
  • 添加IHDR块并使用新的大小,格式,深度进行更新
  • 添加IDAT块
    • 对每条扫描线进行编码(为简单起见,您可以使用过滤器0,但它会增加尺寸)
    • 使用zlib和add来清除数据.使用压缩大小更新块的大小
    • 计算CRC-32校验和
  • 添加IEND块

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

例如:

var arr = [];
arr.push(taSig);   // ta* = typed array
arr.push(taIHDR);
arr.push(taIDAT);
arr.push(taIEND);
Run Code Online (Sandbox Code Playgroud)

然后将数组传递给Blob:

var blob = new Blob(arr, {type: "image/png"});
Run Code Online (Sandbox Code Playgroud)

完整的PNG文件格式规范可以在这里找到.

我将建议您查看我的pngtoy(PNG解析器和解码器,MIT lic.)以获取详细信息.它执行与上述类似的步骤以获得原始解码位图.