在PHP中添加一个PNG pHYs块

5
我正在尝试在生成PNG之前添加有关物理大小的信息以便打印。
阅读libpng文档和pHYs块规范很有帮助,但我似乎无法解决问题。
我已经尝试以最手动和简单的方式添加此块,但是.png文件最终损坏了。我错过了编码技巧吗?
对于CRC计算,我使用了this site的32位结果,并插入了下面代码给我的块的ASCII值。
$encoded = $_POST['imgdata'];
$encoded = str_replace(' ', '+', $encoded);
$decoded=  base64_decode($encoded);

$test = explode('IDAT',$decoded);
$ppu='00000000000000000010111000100011';  //32-bit integer for the pixels per unit
$dppu=bindec($ppu);
$test[0].=sprintf("%c",bindec('00000000000000000000000000001001')) //length, also 32-bit
    .'pHYs'                                                      //type
    .  sprintf("%c",$dppu)                             //Pixels per unit, x axis
    . sprintf("%c",$dppu)                             //Pixels per unit, y axis
    .'1'                                                 //Units in metres (1 byte)
    .  sprintf("%c",  bindec(base_convert('0x0BFAAA7E', 16, 2))) //CRC (32-bit)
    .'IDAT';
$fintest=implode($test);

echo $fintest;

请告诉我,像这样黑客攻击是否有可能成功。我也不确定我的32位整数:像我这样用零填充它们是使它们成为32位的正确方法吗?


1
“%c” 不只是打印8位字符吗?$ppu、$dppu和CRC都是4字节(32位)整数。你可以将每个整数拆分为4个字节,然后对每个字节使用sprintf()函数。 - Glenn Randers-Pehrson
这些函数 explodeimplode 是否执行我猜测的操作:仅对 IDAT 块进行解压缩和重新压缩?那是错误的:pHYs 是一个单独的块,与压缩的 IDAT 无关。 - Jongware
“pHYs”块应该放在“IDAT”之前,我正在使用块名称将其分离以便在此处添加。我会更深入地了解“%c”格式标准,我不知道位数不能从整数转换为相应的ASCII码。谢谢。 - Alex
2个回答

2

在扩展Alex的答案并纠正他代码中的小错误后,我将尝试提供一个工作示例,演示如何在纯PHP中添加PNG pHYs块或设置PNG图像分辨率,而不需要重采样图像。以下代码从file.png读取数据,并将修改后的图像发送到标准输出。

<?php

  // Read file data
  $data = file_get_contents('file.png');

  // Pixels per inch
  $ppi = 300;

  // Unit conversion PPI to PPM
  $ppm = round($ppi * 100 / 2.54);

  $ppm_bin = str_pad(base_convert($ppm, 10, 2), 32, '0', STR_PAD_LEFT);

  // Split PNG data at first IDAT chunk
  $data_splitted = explode('IDAT', $data, 2);

  // Generate "pHYs" chunk data

  // 4-byte data length
  $length_bin = '00000000000000000000000000001001';
  // 4-byte type or 'pHYs'
  $chunk_name_bin = '01110000010010000101100101110011';
  // 9-byte data
  $ppu_data_bin = 
           $ppm_bin // Pixels per unit, x axis
          .$ppm_bin // Pixels per unit, y axis
          .'00000001'; // units - 1 for meters
  // Calculate 4-byte CRC
  $hash_buffer = '';
  foreach(str_split($chunk_name_bin.$ppu_data_bin, 8) as $b)
    $hash_buffer .= chr(bindec($b));
  $crc_bin = str_pad(base_convert(crc32($hash_buffer), 10, 2), 32, '0', STR_PAD_LEFT);
  // Create chunk binary string
  $binstring = $length_bin
          . $chunk_name_bin
          . $ppu_data_bin
          . $crc_bin;

  // Convert binary string to raw
  $phys_chunk_raw = '';
  foreach(str_split($binstring, 8) as $b)
    $phys_chunk_raw .= chr(bindec($b));

  // Insert "pHYs" chunk before first IDAT tag
  $new_image_data = substr($data_splitted[0], 0, strlen($data_splitted[0]) - 4)
        . $phys_chunk_raw
        . substr($data_splitted[0], strlen($data_splitted[0]) - 4, 4)
        . 'IDAT'
        . $data_splitted[1];

  // Output modified image
  header("Content-Type: image/png");
  echo $new_image_data;

?>

或者,更加优雅的代码段可以实现同样的功能(来自habrahabr.ru):
<?php

  // Read file data
  $data = file_get_contents('file.png');

  // Pixels per inch
  $ppi = 300;

  $incPos = strpos($data, 'IDAT') - 4; // Get chunk position
  $chunk = 'pHYs'.pack('NNc', round($ppi/0.0254), round($ppi/0.0254), 1); // Pack chunk type + chunk data
  $incData = pack('N', 9).$chunk.pack('N', crc32($chunk)); // Append chunk's size at beginning, it's crc at the end
  $new_image_data = substr_replace($data, $incData, $incPos, 0);

  // Output modified image
  header("Content-Type: image/png");
  echo $new_image_data;

?>

唯一需要记住的是:这种方法只适用于没有pHYs块的PNG文件。例如,由PHP GD或JavaScript HTMLCanvasElement.toDataURL()生成的文件。如果您的PNG已经有了pHYs块,那么您必须找到并修改现有的块。


2
在这一天的努力中,我发现了一种逐字节翻译的方法。按照文档,pHYs块需要以下内容:
  • 4个字节用于块(仅数据)长度
  • 4个字节用于块类型(字符“pHYs”)
  • 9个字节用于块数据:每个x和y密度各4个字节,单位选择1个字节
  • 4个字节用于CRC
我将所有这些写成二进制字符串,并根据需要填充字节0。然后,chr()函数正确地对其进行编码以在PNG文件中使用,而不像我在问题中使用的sprintf("%c",$string)方法。以下是代码:
$encoded = $_POST['imgdata'];
$encoded = str_replace(' ', '+', $encoded);
$decoded=  base64_decode($encoded);

$binstring='00000000000000000000000000001001' //4-byte length
        . '01110000010010000101100101110011' //4-byte type or 'pHYs'
        . '000000000000000000101110001000110000000000000000001011100010001100000001' //9-byte data
        . base_convert('0x0BFAAA7E', 16, 2); //4-byte CRC
foreach (str_split($binstring,8) as $b) {
    $on.=chr(bindec($b));
}
$on变量随后被附加到4个字节的IDAT块长度之前的位置。

我想将PNG的DPI更新为300。你可以请提供代码吗?(trytusar@gmail.com - Tusar
将代码正文如上所示粘贴即可得到您想要的结果。 - Alex

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