5
votes

I'm trying to tack on some information about the physical size for printing my PNGs just before they are generated.

Reading the libpng doc and the pHYs chunk specifications has been helpful but I just can't seem to crack it.

I have tried adding this chunk in the most manual and simplest way possible, however, the .png file ends up corrupted. Am I missing an encoding trick?

For the CRC computation I have used the 32-bit result of this site, having plugged in the ASCII values for the chunk that the code below gives me.

$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;

Please let me know whether hacking it in like this is likely to work. I am also unsure about my 32-bit integers: is zero-padding them as I am doing the correct way to make them 32-bit?

2
Isn't "%c" going to just print an 8-bit character? $ppu, $dppu, and the CRC are 4-byte (32-bit) integers. You could split each of those into 4 bytes and then sprintf() each byte.Glenn Randers-Pehrson
Do these functions explode and implode what I suspect they do: uncompressing and then recompressing only the IDAT chunk? That's an error: pHYs is a separate chunk, independent of the compressed IDAT.Jongware
The pHYs chunk should be placed before the IDAT, I am exploding it at the chunk name to append it at this point. I will look more into the "%c" formatting standard, I was not aware the bit count wouldn't "translate" from an integer into the corresponding ASCII. ThanksAlex

2 Answers

2
votes

After having spent the day on this, I found that the way to do this was to translate everything byte by byte. Following the docs, the pHYs chunk takes:

  • 4 bytes for chunk (only the data) length
  • 4 bytes for chunk type (the characters 'pHYs')
  • 9 bytes for chunk data: 4 bytes each for x- and y-density and 1 byte for unit choice
  • 4 bytes for the CRC

I wrote all this as a binary string, padding the bytes with 0 as necessary. It then turns out that the chr() function correctly encodes this to be used in the PNG file, unlike the sprintf("%c",$string) method I use in the question. Here is the code

$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));
}

The $on variable is then appended to the position just before the 4 bytes of the IDAT chunk length.

1
votes

Extending Alex's answer and correcting minor errors in his code, I'll try to provide a working example on how to append a PNG pHYs chunk, or set PNG image resolution in pure PHP without resampling image. The following code reads data from file.png and sends modified image to the standard output.

<?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;

?>

Or, more elegant snippet that does the same thing (found at 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;

?>

The only thing that you must remember: this approach works only with PNG files that have no pHYs chunk. For example, generated by PHP GD, or JavaScript HTMLCanvasElement.toDataURL(). If your PNG already has a pHYs chunk, you must find and modify existing chunk.