Can I programmatically determine if a PNG is animated?
Asked Answered
S

5

20

I have PNG (as well as JPEG) images uploaded to my site.

They should be static (i.e. one frame).

There is such thing as APNG.

Bouncy ball

(it will be animated in Firefox).

According to the Wikipedia article...

APNG hides the subsequent frames in PNG ancillary chunks in such a way that APNG-unaware applications would ignore them, but there are otherwise no changes to the format to allow software to distinguish between animated and non-animated images.

Does this mean it is impossible to determine if a PNG is animated with code?

If it is possible, can you please point me in the right direction PHP wise (GD, ImageMagick)?

Shipowner answered 24/12, 2010 at 8:37 Comment(1)
till your question, i was unaware of animated png files. thanx.Ratio
A
18

APNG images are designed to be "camouflaged" as PNG for readers that not support them. That is, if a reader does not support them, it will just assume it is a normal PNG file and display only the first frame. That means that they have the same MIME type as PNG (image/png), they have the same magic number (89 50 4e 47 0d 0a 1a 0a) and generally they're saved with the same extension (although that is not really a good way to check for a file type).

So, how do you distinguish them? APNG have a "acTL" chunk in them. So, if you search for the string acTL (or, in hex, 61 63 54 4C (the 4 bytes before the chunk marker (i.e. 00 00 00 08) are the size of the chunk in big endian format, without counting the size, marker, or CRC32 at the end of the field)) you should be pretty good. To get it even better, check that this chunk appears before the first occurrence of the "IDAT" chunk (just look for IDAT).

This code (taken from http://foone.org/apng/identify_apng.php ) will do the trick:

<?php
# Identifies APNGs
# Written by Coda, functionified by Foone/Popcorn Mariachi#!9i78bPeIxI
# This code is in the public domain
# identify_apng returns:
# true if the file is an APNG
# false if it is any other sort of file (it is not checked for PNG validity)
# takes on argument, a filename.
function identify_apng($filename)
    {
    $img_bytes = file_get_contents($filename);
    if ($img_bytes)
        {
        if(strpos(substr($img_bytes, 0, strpos($img_bytes, 'IDAT')), 
                 'acTL')!==false)
            {
        return true;
        }
        }
    return false;
    }
?>
Allison answered 24/12, 2010 at 8:46 Comment(4)
Awesome! Though of course, I'd rather just return that condition :) +1Shipowner
@alex: sure, that would be nicer. I just copied the code as it was (I'm not the author) ;)Allison
@MarcusJ Rather than leaving rude comments, you should go ahead and edit my answer to make it better.Allison
The given link is broken, Thank god you copied that here. That's why you shouldn't just link ;)Collete
S
6

AFAIK, libraries that do not support APNG will just take the first frame of the PNG. In your case, you could just create a new image from the APNG (or PNG, JPEG, etc.) and re-save it as PNG. It should strip the animation data if using GD, unless the library's been updated to support APNG.

Sletten answered 24/12, 2010 at 8:41 Comment(1)
I didn't consider this - all my images are processed via GD. This is a good recommendation. +1Shipowner
B
3

I'd like to suggest a more optimised version, which doesn't read the whole file, as those could be quite big, and still rely on the acTL before IDAT rule:

function identify_apng($filepath) {
    $apng = false;

    $fh = fopen($filepath, 'r');
    $previousdata = '';
    while (!feof($fh)) {
        $data = fread($fh, 1024);
        if (strpos($data, 'acTL') !== false) {
            $apng = true;
            break;
        } elseif (strpos($previousdata.$data, 'acTL') !== false) {
            $apng = true;
            break;
        } elseif (strpos($data, 'IDAT') !== false) {
            break;
        } elseif (strpos($previousdata.$data, 'IDAT') !== false) {
            break;
        }

        $previousdata = $data;
    }

    fclose($fh);

    return $apng;
}

Speed is enhanced from 5x to 10x or more depending on how big the file is, and it also uses a lot less memory.

NB: this maybe could be tweaked more with the size given to fread or with the concatenation of the previous chunk with the current one. By the way, we need this concatenation as the acTL/IDAT word might be split between two read chunks.

Ben answered 7/10, 2018 at 11:17 Comment(0)
T
1

Here is my function that scans the chunk structure, not just substring inside the file (to prevent a false-positive if the acTL substring appears in the metadata instead of the chunk name). For simplicity i used SplFileObject, the speed can be improved by using fopen/fread/fclose directly.

function is_apng(string $filename): bool
{
    $f = new \SplFileObject($filename, 'rb');
    $header = $f->fread(8);
    if ($header !== "\x89PNG\r\n\x1A\n") {
        return false;
    }
    while (!$f->eof()) {
        $bytes = $f->fread(8);
        if (strlen($bytes) < 8) {
            return false;
        }
        $chunk = unpack('Nlength/a4name', $bytes);
        switch ($chunk['name']) {
            case 'acTL':
                return true;
            case 'IDAT':
                return false;
        }
        $f->fseek($chunk['length'] + 4, SEEK_CUR);
    }
    return false;
}
Tavares answered 2/8, 2021 at 8:8 Comment(0)
B
1

If any JS coder stumbles in here - Javascript version of https://mcmap.net/q/620547/-can-i-programmatically-determine-if-a-png-is-animated

const identifyApng = (byteString) => {
  if (byteString.length > 0) {
    const idatPos = byteString.indexOf('IDAT')
    if(byteString.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0) {
      return true
    }
  }
  return false
}
Breathy answered 23/5, 2022 at 20:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.