如何使用 PNG 的 IDAT 块?

Posted

技术标签:

【中文标题】如何使用 PNG 的 IDAT 块?【英文标题】:How to use PNG's IDAT chunk? 【发布时间】:2011-11-09 17:00:55 【问题描述】:

我试图了解数据是如何存储到 IDAT 块中的。我正在编写一个小的 php 类,我可以检索大部分块信息,但是我从 IDAT 获得的信息与我的图像像素不匹配:

2×2px 真彩色,alpha (bitdepth 8)。

但是当我这样解释 IDAT 数据时:

current(unpack('H*',gzuncompress($idat_data)));

我明白了

00000000ffffff00ffffff000000

我不明白它如何匹配像素。还是我的代码破坏了数据?

感谢您的帮助!

编辑:我明白了

08d705c101010000008010ff4f1750a93029e405fb

由于是十六进制压缩数据,所以解压后似乎丢失了几个字节。

【问题讨论】:

【参考方案1】:

使用 gzinflate 但先跳过前 2 个字节和后 4 个字节。

$contents = file_get_contents($in_filename);
$pos = 8; // skip header

$color_types = array('Greyscale','unknown','Truecolour','Indexed-color','Greyscale with alpha','unknown','Truecolor with alpha');
$len = strlen($contents);
$safety = 1000;
do 
    list($unused,$chunk_len) = unpack('N', substr($contents,$pos,4));

    $chunk_type = substr($contents,$pos+4,4);

    $chunk_data = substr($contents,$pos+8,$chunk_len);

    list($unused,$chunk_crc) = unpack('N', substr($contents,$pos+8+$chunk_len,4));
    echo "chunk length:$chunk_len(dec) 0x" . sprintf('%08x',$chunk_len) . "h<br>\n";
    echo "chunk crc   :0x" . sprintf('%08x',$chunk_crc) . "h<br>\n";
    echo "chunk type  :$chunk_type<br>\n";
    echo "chunk data  $chunk_type bytes:<br>\n"  . chunk_split(bin2hex($chunk_data)) . "<br>\n";
    switch($chunk_type) 
        case 'IHDR':
        list($unused,$width,$height) = unpack('N2', substr($chunk_data,0,8));
        list($unused,$depth,$Color_type,$Compression_method,$Filter_method,$Interlace_method) = unpack('C*', substr($chunk_data,8));
        echo "Width:$width,Height:$height,depth:$depth,Color_type:$Color_type(" . $color_types[$Color_type] . "),Compression_method:$Compression_method,Filter_method:$Filter_method,Interlace_method:$Interlace_method<br>\n";
        $bytes_per_pixel = $depth / 8;
        break;

        case 'PLTE':
        $palette = array();
        for($i=0;$i<$chunk_len;$i+=3) 
            $tupl = bin2hex(substr($chunk_data,$i,3));
            $palette[] = $tupl;
            if($i && ($i % 30 == 0)) 
                echo "<br>\n";
            
            echo '<span style="color:' . $tupl . ';">[' . $tupl . ']</span>';
        
        echo print_r($palette,true) . "<br>";
        break;

        case 'IDAT':
        $compressed = substr($chunk_data,2,$chunk_len - 6); // 2 bytes on the front and 4 at the end
        $decompressed = gzinflate($compressed);
        echo "decompressed chunk data " . strlen($decompressed) . " bytes:<br>\n"  . chunk_split(bin2hex($decompressed),2 + $width * $bytes_per_pixel * 2) . "<br>\n";
        for($row=0; $row<$height; $row++) 
            for($col=1; $col<=$width; $col++) 
                $index = (int)substr($decompressed,((int)$row*($width+1)+$col),1);
                echo '<span style="color:' . $palette[$index] . ';">' . $index . '</span>';
            
            echo "<br>\n";
        
        // TODO use filters described here:
        // http://www.w3.org/TR/PNG/#9Filters
        // first byte of scan line is filter type
        break;

    
    $pos += $chunk_len + 12;
    echo "<hr>";
 while(($pos < $len) && --$safety);

【讨论】:

谢谢,inflate 现在可以工作,但我得到“00000000ffffff00ffffff000000”(14 字节),它们是如何用来获取像素的? 为了获得良好的压缩效果,PNG 格式会在压缩前应用过滤器。过滤器执行以下操作:如果两条扫描线一个接一个几乎相同,则与上方像素匹配的下部行上的像素将更改为零。所以当你完成后,你会有一堆零,压缩真的很好。所以你需要在解压后反转它并撤消过滤器。见w3.org/TR/PNG/#9Filters 对,过滤器“将扫描行中的字节序列转换为等长的字节序列,前面是过滤器类型字节”。那么我不应该有一个 18 字节的未压缩数据(1 个“字节深度”* 4 个通道 * 4 个像素 + 2 个过滤器)吗? 看起来不错。也许真的没有alpha通道???如果第一个字节有其他含义并且没有alpha,那么你将有00(神秘字节)000000(rgb)ffffff(rgb)+另一个神秘字节+ffffff和000000。所以也许每条扫描线都有一个字节来描述该行的过滤方案。抱歉,我已经很久没有对此进行任何编码了。 就是这样!我的错误是认为 Gimp 使用 Alpha 通道保存了我的图像。但是“真彩色”的图像类型为 2,因此它只有 3 个通道(R、V、B)。神秘字节是用于每个扫描线的过滤器类型(在这种情况下没有)。谢谢!【参考方案2】:
00000000 ffffff00 ffffff00 0000xxxx
black    white    white    black

这就是我能说的(这是正确的)......但你最后缺少 2 个字节。

【讨论】:

我以为每条扫描线都有一个过滤器类型字节?丢失的字节可能来自糟糕的解压缩算法吗? 我根本没有读过PNG,但你提供的数据似乎与你应该得到的数据相对应,除了它不是全部......所以我个人无法帮助您了解为什么会发生这种情况:w3.org/TR/PNG/#11IDAT@leonbloy 对于多个 IDAT 块可能是正确的,但我觉得奇怪的是这么小的块会被拆分...您确定要解压缩所有字节? 谢谢,但w3.org/TR/PNG/#4Concepts.EncodingFiltering 似乎过滤器类型应该存在于数据中,所以会丢失更多字节?? (我知道在我的情况下只有一个 IDAT 块^^) 过滤器方法是 IHDR 块的一部分,而不是看起来的 IDAT 块......但我无法解码过滤器实际工作方式的描述。 w3.org/TR/PNG/#11IHDR 每行的第一个 byte 在这里是00(PNG 行过滤器)。之后,你会得到两个 RGB 三元组:00 00 00ff ff ff 用于第一行,ff ff ff00 00 00 用于第二行。【参考方案3】:

要添加到@Andreas (+1) 解析,需要注意两点:

    一个 PNG 文件可以有(并且经常有)很多 IDAT 块,它们必须连接起来才能恢复压缩的 zlib 流。 http://www.w3.org/TR/PNG/#10CompressionFSL

    Gzip/Compress/Deflate 都是相关的,但并不完全相同。 PNG 使用deflate/inflate。我会尝试gzdeflate/gzinflate

【讨论】:

我试过了,但使用 gzinflate =/ 时出现数据错误(我的图像只有一个 IDAT 块) @MatTheCat 我已经使用 Java Deflater/Infalter 类实现了 PNG 读/写,它完美地工作。也许您会尝试剥离前两个字节? gzinflate(substr($idat_data, 2) ? php.net/manual/en/function.gzinflate.php#70875 我刚刚注意到 IDAT 块的长度部分小于其数据的长度,我认为这是问题所在,但我猜不出原因 @MatTheCat 的 IDAT 长度部分?我似乎找不到任何东西? @MatTheCat “长度只计算数据字段,不计算本身、块类型或 CRC。”在我看来,您很可能将其用作整个 IDAT 块的长度。刚刚仔细检查了你上面的十六进制代码,我发现 DATA 是 21 个字节。

以上是关于如何使用 PNG 的 IDAT 块?的主要内容,如果未能解决你的问题,请参考以下文章

工具 PNG Debugger 的安装使用

ctfshow 做题 MISC入门 模块 11-20

.NET Image.Save 偶尔会生成一个带有错误 IDAT 块的 PNG

带 Alpha 解码的 PNG 真彩色

Zlib在C中解压缩具有未知压缩长度的字节

Rancher如何对接Ceph-RBD块存储