如何使用PNG的IDAT块?

12

我正在尝试理解数据如何存储到IDAT块中。我正在编写一个小的PHP类,可以检索大多数块信息,但我获取的IDAT块信息与我的图像像素不匹配:

enter image description here 这是一个带有alpha通道的2×2像素真彩色图像(位深度为8)。

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

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

我得到的是

00000000ffffff00ffffff000000

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

谢谢您的帮助!

编辑:我得到的是

08d705c101010000008010ff4f1750a93029e405fb

作为十六进制压缩数据,所以看起来在解压缩后我丢失了几个字节。

输入图像描述

3个回答

10

使用gzinflate函数,但跳过前两个字节和最后四个字节。

$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个字节),它们如何用于获取像素? - MatTheCat
为了获得良好的压缩效果,PNG格式在压缩之前应用滤镜。这些滤镜会执行一些操作,例如:如果两个相邻的扫描行几乎相同,则匹配上方像素的下方像素将被更改为零。因此,当完成后,您将拥有大量的零,并且压缩效果非常好。因此,在解压缩后需要撤消这些滤镜。请参见http://www.w3.org/TR/PNG/#9Filters。 - Charlie
1
正确,并且过滤器“将扫描线中的字节序列转换为由过滤器类型字节前导的等长度字节序列”。 那么我难道不应该有18个字节的未压缩数据(1个“位深度”* 4个通道* 4个像素+ 2个过滤器)吗? - MatTheCat
似乎是对的。也许真的没有 Alpha 通道?如果第一个字节有其他含义,而且没有 Alpha 通道,那么你会得到 00(神秘字节)000000(RGB)ffffff(RGB)+ 另一个神秘字节 + ffffff 和 000000。因此,每个扫描行可能都有一个字节来描述该行的滤波方案。抱歉,我已经太久没写这方面的代码了。 - Charlie
就是这样!我的错误在于认为Gimp使用alpha通道保存了我的图像。但是,图像类型为“真彩色”,因此只有3个通道(R、V、B)。神秘字节是用于每个扫描线的滤波器类型(在这种情况下没有)。谢谢! - MatTheCat

4
00000000 ffffff00 ffffff00 0000xxxx
black    white    white    black

这是我能告诉你的(是正确的)...但你遗漏了末尾的两个字节。


2
我原以为每个扫描线都有一种过滤类型的字节?缺失的字节可能来自于错误的解压算法吗? - MatTheCat
我对PNG并不是很了解,但你提供的数据似乎对应于你应该得到的内容,除了它并不完整...所以我个人无法帮助你弄清楚为什么会发生这种情况:http://www.w3.org/TR/PNG/#11IDAT @leonbloy可能是对的,关于多个IDAT块,但我觉得一个如此小的块被分割开很奇怪...你确定你解压了所有的字节吗? - Andreas
是的,但如果我理解正确,每个扫描线中应该有一个过滤器类型。但似乎我错过了其他东西;我已经编辑了我的问题。 - MatTheCat
据我所知,从我现在所读的很少内容来看...它们不是都使用相同的过滤器类型吗?@MatTheCat - Andreas
2
这里每行的第一个字节00(PNG行过滤器)。之后,您会得到两个RGB三元组:第一行为00 00 00ff ff ff,第二行为ff ff ff00 00 00 - Jongware
显示剩余2条评论

4
添加到@Andreas解析的内容,有两个要注意的地方:
  1. PNG文件可以有(通常有)多个IDAT块,它们必须连接在一起才能恢复压缩的zlib流。 http://www.w3.org/TR/PNG/#10CompressionFSL

  2. Gzip/Compress/Deflate都是相关但不完全相同的。PNG使用deflate/inflate。我建议尝试gzdeflate/gzinflate函数。


我尝试了,但在使用gzinflate时出现数据错误 =/(我的图像只有一个IDAT块) - MatTheCat
@MatTheCat 我已经使用Java Deflater/Infalter类实现了PNG读写,它完美无缺地工作。也许你可以尝试去掉前两个字节?gzinflate(substr($idat_data, 2) ? http://www.php.net/manual/en/function.gzinflate.php#70875 - leonbloy
我刚刚注意到IDAT块的长度部分比其数据的长度要小,我认为这就是问题所在,但我猜不出原因。 - MatTheCat
@MatTheCat 无论如何,我在 PHP 中复制了它,出现了同样的问题,末尾缺少 2 个字节...并且过滤方法是 NONE。 - Andreas
IHDR告诉我们过滤方法为0,因此每个扫描行应该有一个过滤类型(http://www.w3.org/TR/PNG/#9Filter-types)。 - MatTheCat
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接