7

I am working on a project where a user can upload webp images. I know how to convert webp images into jpg/png, but am stuck on how to identify whether the webp image is static (non-animated) or animated.

I want to identify them because I use different commands for conversion:

Command for non-animated webp to jpg:

dwebp nonanimated.webp -o jpg.jpg

Command for animated webp to non-animated webp (takes 2nd frame):

webpmux -get frame 2 animated.webp -o nonanimated.webp

But I cannot find a single command that handle both cases.

I am using PHP on the server side, and HTML and Javascript for frontend.

John Kary
  • 6,703
  • 1
  • 24
  • 24
HarisH Sharma
  • 1,101
  • 1
  • 11
  • 38

7 Answers7

7

After a lot of investigation i found that animated webp images always conatins some string, when open in a text editor and non animated images doesn't. The strings are ANMF and ANIM. I checked these string in all webp images which i have. So this is perfect for me. Here are some solutions in PHP, Javascript and Shell Script:

In PHP:

<?php
function isWebpAnimated($src){
    $webpContents = file_get_contents($src);
    $where = strpos($webpContents, "ANMF");
    if ($where !== FALSE){
        // animated
        $isAnimated = true;
    }
    else{
        // non animated
        $isAnimated = false;
    }
    return $isAnimated;
}
?>

In Javascript:

function isAnimatedGif(src) {
    var request = new XMLHttpRequest();
    request.open('GET', src, true);
    request.addEventListener('load', function () {
        if(request.response.indexOf("ANMF") != -1){
            // animated
            alert(true);
        }
        else{
            // non animated
            alert(false);
        }
    });
    request.send();
}

But In case of large images PHP and Javascript not working well, So best Solution is to use Shell Script, If you have Ubuntu.

In Shell Script:

echo $(grep -c "ANMF" ~/animated.webp)

return 0 if non animated, else non zero value for animated.

HarisH Sharma
  • 1,101
  • 1
  • 11
  • 38
  • 1
    Note that this solution will produce false-positives in rare cases when some bytes in the file happen to contain the same binary values as the string "ANMF". Parsing the webp header as suggested in the other answers would be more reliable. – Alexander Revo Nov 17 '20 at 14:53
  • 3
    Also it would work equally fast for files of any size, since you only need to read a few bytes at the very beginning of the file. – Alexander Revo Nov 17 '20 at 15:11
  • 1
    In PHP, the `if`, `else` and `return` blocks can be replaced by `return ($where !== FALSE);`. – A.L May 04 '23 at 13:49
7

There are flags in Webp header, ANIMATION among others. Small function to check it:

function isWebpAnimated($fn){
  $result = false;
  $fh = fopen($fn, "rb"); 
  fseek($fh, 12);
  if(fread($fh, 4) === 'VP8X'){
    fseek($fh, 16);
    $myByte = fread($fh, 1);
    $result = ((ord($myByte) >> 1) & 1)?true:false;
  }
  fclose($fh);
  return $result;
}

ANIM and ANMF are from next chunk headers.

RIFF container specification

Sven Liivak
  • 1,323
  • 9
  • 10
  • `?true:false` can be removed, it's not necessary to change a boolean to a boolean. – A.L May 04 '23 at 13:48
5

According Sven Liivak's isWebpAnimated()... there is a small bug.

fseek($fh, 16);

should be:

fseek($fh, 20);

Because postion 16 is the chunk_size position in VP8X. But we need the flag position which is at 20.

Fixed function:

function isWebpAnimated($fn){
  $result = false;
  $fh = fopen($fn, "rb"); 
  fseek($fh, 12);
  if(fread($fh, 4) === 'VP8X'){
    fseek($fh, 20);
    $myByte = fread($fh, 1);
    $result = ((ord($myByte) >> 1) & 1)?true:false;
  }
  fclose($fh);
  return $result;
}
2

This is my java code, it works for me.

static boolean isWebpAnimated(InputStream in) {
        boolean result = false;
        try {
            in.skip(12);
            byte[] buf = new byte[4];
            int i = in.read(buf);
            if ("VP8X".equals(new String(buf, 0, i))) {
                in.skip(12);
                result = (in.read(buf) == 4 && (buf[3] & 0x00000002) != 0);
            }
        } catch (Exception e) {
        } finally {
            try {
                in.close();
            } catch (Exception e) {
            }
        }
        return result;
    }
ccomangee
  • 31
  • 2
2

Fixing @ccomangee's solution. Some static webp images would be detected as animated and can cause issue in the application.

I had extracted webp frames and saved as webp images and tried to identify by checking VP8X signature and it exists in given position although It is static image. So if there is VP8X it does not mean that the image would be animated having more than one frames.

I tried few images with my solution and the result is below:

                             riff   webp  vp8*   anim
OK(anim-trans).webp:      [ RIFF | WEBP | VP8X | ANIM ]
Cuppy(static-trans).webp: [ RIFF | WEBP | VP8L | NA?  ]
glass(anim-solid).webp:   [ RIFF | WEBP | VP8X | ANIM ]
sunset(anim-trans).webp:  [ RIFF | WEBP | VP8X | ANIM ]
atom(anim_solid).webp:    [ RIFF | WEBP | VP8X | ANIM ]
spread(anim-trans).webp:  [ RIFF | WEBP | VP8X | ANIM ]
heart(static-trans).webp: [ RIFF | WEBP | VP8X | NA?  ] 
ludo(static-trans).webp:  [ RIFF | WEBP | VP8X | NA?  ]
scene(static_solid).webp: [ RIFF | WEBP | VP8  | NA?  ]

Here all images are named according to its type. anim-trans: animated image contains transparency (alpha channel support) anim-solid: animated image having no transparency static-trans and static-solid are static images.

VP8L is loseless webp and VP8X contains extended features. VP8 is surely static image.

If VP8X exists it could be static or animated most images would be animated.

The solution is

Read 4 bytes -> 'RIFF' Skip 4 bytes Read 4 bytes -> 'WEBP' Read 4 bytes -> 'VP8X' / 'VP8L' / 'VP8' skip 14 bytes Read 4 bytes -> 'ANIM'

Java Code:

 public static boolean check(File file) {
            boolean riff = false;
            boolean webp = false;
            boolean vp8x = false;
            boolean anim = false;
        try (InputStream in = new FileInputStream(file)) {

            byte[] buf = new byte[4];
            int i = in.read(buf); // 4
            
            if(buf[0] == 0x52 && buf[1] == 0x49 && buf[2]==0x46 && buf[3] == buf[2] )
                riff = true;

            in.skip(4); // ???? (8+)
            i = in.read(buf); // (12+)
            if(buf[0] == 0x57 && buf[1] == 0x45 && buf[2]==0x42 && buf[3] == 0x50 )
                    webp = true   ;             

            i = in.read(buf); // (16+)
            if(buf[0] == 0x41 && buf[1] == 0x4e && buf[2]==0x49 && buf[3] == 0x4d );
                vp8x = true;
                
            in.skip(14); // then next 4 should contain ANIM - 41 4e 49 4d
            i = in.read(buf);
            if(buf[0] == 0x41 && buf[1] == 0x4e && buf[2]==0x49 && buf[3] == 0x4d )
                anim = true;

        } catch (Exception e) {
            System.out.println("errrrrr "+e.getMessage());
        }
        return riff && webp && anim;

    }

you can directly read WEBP by skipping 8 bytes then count and skipp all chunks before ANIM and read that position if ANIM exist then its animated webp image else static.

File layout of webp images https://developers.google.com/speed/webp/docs/riff_container#example_file_layouts

Ref: Google WEBP specification https://developers.google.com/speed/webp/docs/riff_container

CrackerKSR
  • 1,380
  • 1
  • 11
  • 29
1
def is_webp_animation(img_content):
    # https://developers.google.com/speed/webp/docs/riff_container#extended_file_format
    # webp Animation image.
    return len(img_content) > 20 and img_content[0:4] == b'RIFF' and img_content[8:12] == b'WEBP' \
            and img_content[12:16] == b'VP8X' \
            and int.from_bytes(img_content[20:21], 'little') & 2 == 2
nameldk
  • 27
  • 4
1

This is my solution in BASH (Linux). Work in Debian 12 without any needed softwares added. Search and COPY animated webp files to desktop. It is easy to modify my script to accommodate your need. Some idea come from here.

#!/bin/bash
file=$(zenity  --file-selection --filename=$HOME/ --title="Choose a directory to convert all file" --directory)
rm "/dev/shm/findaniwebp.txt" 2> /dev/null
rm "/dev/shm/findfiles.txt" 2> /dev/null
find "$file" -iname '*.webp' >> "/dev/shm/findfiles.txt" 2>/dev/null

{
result=0
input="/dev/shm/findfiles.txt"
while IFS= read -r "line"
do
result=$(echo $(grep -c "ANMF" "$line"))
if [ "$result" -ge 10 ]; then
echo "Animated webp is found !"
echo $line
echo $line >> "/dev/shm/findaniwebp.txt"
fi
done < "$input"
}

if [ ! -f "/dev/shm/findaniwebp.txt" ]
then
aniwebp=0
else
aniwebp=$(wc -l < "/dev/shm/findaniwebp.txt")
echo "Finding finish (webp only) , with file to move : $aniwebp"
fi

if [ "$aniwebp" -ge "1" ]; then
if zenity --no-wrap --question --text="Do you want to COPY theseS fileS to $HOME/Desktop ?"
then
{
input="/dev/shm/findaniwebp.txt"
while IFS= read -r "line"
do
cp "$line" "$HOME"/Desktop
echo file moved...
done < "$input"
}
fi
else
echo "NO animated webp found!"
fi

rm "/dev/shm/findaniwebp.txt" 2> /dev/null
rm "/dev/shm/findfiles.txt" 2> /dev/null
read -n 1 -s -r -p "Press ENTER key to exit !"
exit