Splitting APNG into png images with PHP

1.1k views Asked by At

I have a php code that splits an animated PNG image (APNG) into frames, but there is some kind of error. The images that are created by the function are not valid (PHP does not return an image resource in PHP imagecreatefrompng() function (neither it's displayed in Firefox browser).

Quoting the function from How can I split animated PNG with PHP? by James Holderness

function splitapng($data) {
  $parts = array();

  // Save the PNG signature   
  $signature = substr($data, 0, 8);
  $offset = 8;
  $size = strlen($data);
  while ($offset < $size) {
    // Read the chunk length
    $length = substr($data, $offset, 4);
    $offset += 4;

    // Read the chunk type
    $type = substr($data, $offset, 4);
    $offset += 4;

    // Unpack the length and read the chunk data including 4 byte CRC
    $ilength = unpack('Nlength', $length);
    $ilength = $ilength['length'];
    $chunk = substr($data, $offset, $ilength+4); 
    $offset += $ilength+4;

    if ($type == 'IHDR')
      $header = $length . $type . $chunk;  // save the header chunk
    else if ($type == 'IEND')
      $end = $length . $type . $chunk;     // save the end chunk
    else if ($type == 'IDAT') 
      $parts[] = $length . $type . $chunk; // save the first frame
    else if ($type == 'fdAT') {
      // Animation frames need a bit of tweaking.
      // We need to drop the first 4 bytes and set the correct type.
      $length = pack('N', $ilength-4);
      $type = 'IDAT';
      $chunk = substr($chunk,4);
      $parts[] = $length . $type . $chunk;
    }
  }

  // Now we just add the signature, header, and end chunks to every part.
  for ($i = 0; $i < count($parts); $i++) {
    $parts[$i] = $signature . $header . $parts[$i] . $end;
  }

  return $parts;
}

Saving the parts is performed as follows

$filename = 'example.png';

$handle = fopen($filename, 'rb');
$filesize = filesize($filename);
$data = fread($handle, $filesize);
fclose($handle);

$parts = splitapng($data);

for ($i = 0; $i < count($parts); $i++) {
  $handle = fopen("part-$i.png",'wb');
  fwrite($handle,$parts[$i]);
  fclose($handle);
}

I tried modifying headers but no luck. Please, help!

2

There are 2 answers

0
Glenn Randers-Pehrson On BEST ANSWER

The CRC needs to be recomputed when you change a chunk name from fdAT to IDAT, and remove 4 data bytes, to account for the new chunk name and data.

0
Hans-Jürgen Petrich On

I rewrite the splitapng() because crc32 has to be re-calculated. Works now.

function splitapng($data) {
  $parts = array();

  // Save the PNG signature   
  $signature = substr($data, 0, 8); 
  $offset = 8;
  $size = strlen($data);
  while ($offset < $size) {
    // Read the chunk length
    $length = substr($data, $offset, 4); 
    $offset += 4;

    // Read the chunk type
    $type = substr($data, $offset, 4); 
    $offset += 4;

    // Unpack the length and read the chunk data including 4 byte CRC
    $ilength = unpack('Nlength', $length);
    $ilength = $ilength['length'];
    $chunk = substr($data, $offset, $ilength+4); 
    $offset += $ilength+4;

    if ($type == 'IHDR')
      $header = $length . $type . $chunk;  // save the header chunk
    else if ($type == 'IEND')
      $end = $length . $type . $chunk;     // save the end chunk
    else if ($type == 'IDAT') 
      $parts[] = $length . $type . $chunk; // save the first frame
    else if ($type == 'fdAT') {
      // Animation frames need a bit of tweaking.
      // We need to drop the first 4 bytes and set the correct type.
      $length = pack('N', $ilength-4);
      $type = 'IDAT';
      // re-calc crc
      $chunk = substr($chunk,4,-4);
      $chunk .= hash('crc32b',$type.$chunk,true);
      $parts[] = $length . $type . $chunk;
    }   
  }

  // Now we just add the signature, header, and end chunks to every part.
  for ($i = 0; $i < count($parts); $i++) {
    $parts[$i] = $signature . $header . $parts[$i] . $end;
  }

  return $parts;
}