php – How to convert animated GIF to animated WEBP?

Question:

I am using GD to manipulate images in PHP, based on this code

<?php
/**
Función para detectar si una imagen es animación
*/
function is_ani($filename) {  
    if(!($fh = @fopen($filename, 'rb')))
        return false;
    $count = 0;
    while(!feof($fh) && $count < 2) {
        $chunk = fread($fh, 1024 * 100);
        $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
   }

    fclose($fh);
    return $count > 1;
}
?>

I intend to separate the frames inside the GIF (I can even use this library ) and save them in WEBP format.

How can I assemble/create an animated image in WEBP format using GD in PHP?

–EDITED–

PHP version:

PHP 7.2.4 (built: Apr 25 2018 12:05:18) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

gd_info:

#php -r 'print_r(gd_info());'
Array
(
    [GD Version] => bundled (2.1.0 compatible)
    [FreeType Support] =>
    [GIF Read Support] => 1
    [GIF Create Support] => 1
    [JPEG Support] => 1
    [PNG Support] => 1
    [WBMP Support] => 1
    [XPM Support] =>
    [XBM Support] => 1
    [WebP Support] => 1
    [BMP Support] => 1
    [JIS-mapped Japanese Font Support] =>
)

This system is running on Debian 8.

–EDITED 2–

Using the alo-malbarez project as a base and a class to extract the frames of the GIF's, I made a project with a cleaner and clearer extractor class for people who need specific fragments of the GIF. In this project I join both classes to make a GIF to WEBP converter, it still has bugs but I'm working on correcting them.

Answer:

The issue of how you extract the gif frames would be missing, but for that there is a link at the end

Here is a PoC on how to build an animated webp in php using only GD (with webp support)

PART 1 : encode each frame

1) the frame is converted to webp with GD and captured in a variable with ob_start

2) we discard the fileformat header of this stream and save the frameData (starts with "VP8 " and goes to the end) in an array (buffer) along with the width, height and duration info of the frame

3) returns to 1 until there are no more frames

note: to change the compression quality edit the line

imagewebp($image);

putting for example

imagewebp($image, null, 75);

the null is so that it does not save a file

PART 2: build the animated webp according to the specification https://developers.google.com/speed/webp/docs/riff_container

An animated image with EXIF ​​metadata may look as follows:

RIFF/WEBP

+- VP8X (descriptions of features used)
+- ANIM (global animation parameters)
+- ANMF (frame1 parameters + data)
+- ANMF (frame2 parameters + data)
+- ANMF (frame3 parameters + data)
+- ANMF (frame4 parameters + data)
+- EXIF (metadata) <- opcional

0) open stream or set in a var

1) header file format (RIFF+filesize+WEBP)

1.a) calculate the file size (uint32) the file size is the total bytes of the header file -8, or the total bytes of all chunks + 4

2) VP8X chunk header

2.a) calculate chunksize (uint32) = 10

2.b) set the alpha and animation bits to 1

2.c) encode canvas width and height to uint24 (-1)

3) ANIM chunk header

3.a) compute chunksize (uint32) = 6

3.b) background color: BGRA (0,0,0,0)

3.c) loop count (uint16) 0=infinityandbeyond

4) for each frame saved in the array (buffer) PART 1.2

4.a) ANMF chunk header

4.b) calculate chunksize (uint32) = 16 + total bytes in frameData

4.c) X,Y origin of the frame (uint24) (/2)

4.d) width and height of the frame (uint24) (-1)

4.e) duration milliseconds (uint24)

4.f) reserved (6 bits) + alpha blending (1 bit) + discard frame (1 bit)

4.g) the Data frame saved in the buffer PART 1.2

5) save to disk or close stream

#!/usr/bin/env php
<?php

function toUint32($n){
  $ar = unpack("C*", pack("L", $n));
  return $ar;
}
function toUint24($n){
  $ar = unpack("C*", pack("L", $n));
  array_pop($ar);
  return $ar;
}
function toUint16($n){
  $ar = unpack("C*", pack("S", $n));
  return $ar;
}

function bytesToString($bytes){
  return implode(array_map("chr", $bytes));
}

function binaryToBytes($bits){
  $octets = explode(' ', $bits);
  return array_map("bindec", $octets);
}

$oWidth=120;
$oHeight=20;

$frameArray = [];

function getFrameData($image, $msec){
  $w = imagesx($image);
  $h = imagesy($image);

  ob_start();
  imagewebp($image);
  if (ob_get_length() % 2 == 1) :
    echo "\0";
  endif;
  $image_data = ob_get_contents();
  ob_end_clean();
  $frameData = substr($image_data, strpos($image_data, "VP8 "));
  return Array(
    "frameData" => $frameData,
    "duration" => bytesToString(toUint24($msec)),
    "width" => bytesToString(toUint24($w - 1)),
    "height" => bytesToString(toUint24($h -1 )),
  );
}

// Create a blank image and add some text
$im = imagecreatetruecolor($oWidth, $oHeight);
$text_color = imagecolorallocate($im, 233, 14, 91);
imagestring($im, 1, 5, 5,  'WebP with PHP', $text_color);

$frameArray[] = getFrameData($im, 70);
imagedestroy($im);

// Create a blank image and add some text
$im = imagecreatetruecolor($oWidth, $oHeight);
$text_color = imagecolorallocate($im, 14, 233, 91);
imagestring($im, 1, 5, 5,  'WebP with PHP', $text_color);

$frameArray[] = getFrameData($im, 70);
imagedestroy($im);

// create new WEBP
$fileWEBP = "";
$fileHeader = "";
$fileContents = "";

// Chunk HEADER VP8X
$fileContents .="VP8X";
$headChunkSize = bytesToString(toUint32(10));
// bit flags Rsv|I|L|E|X|A|R|                   Reserved
$oVP8XflagsBin = "00010010 00000000 00000000 00000000";
$oVP8Xflags = bytesToString(binaryToBytes($oVP8XflagsBin));
$oCanvasSize = bytesToString(toUint24($oWidth-1)).bytesToString(toUint24($oHeight-1));
$fileContents .= $headChunkSize. $oVP8Xflags. $oCanvasSize;

// Chunk HEADER ANIM
$fileContents .="ANIM";
$animChunkSize = bytesToString(toUint32(6));
// loop count 16bits, 0 = infinito
// bytesToString(toUint16(0));
$oLoopCount = str_repeat(chr(0), 2);
// 32bits BGRA, Blue Green Red Alpha (0,0,0,0)
$oBackGround = str_repeat(chr(0), 4);
$fileContents .= $animChunkSize . $oBackGround . $oLoopCount;

foreach ($frameArray as $frame) :
  // Chunk HEADER ANMF
  $fileContents .="ANMF";
  $frameDataChunkSize = bytesToString(toUint32(strlen($frame['frameData'])+16));
  // frame origin X Y
  // bytesToString(toUint24(originX)) . bytesToString(toUint24(originY))
  // (0,0)
  $fOrigin = str_repeat(chr(0), 6);
  // frame size (uint24) width-1 , (uint24) height-1
  $fSize = $frame['width'].$frame['height'];
  // frame duration in miliseconds (uint24)
  $fDuration = $frame['duration'];
  // frame options bits
  // reserved (6 bits) + alpha blending (1 bit) + descartar frame (1 bit)
  $fFlagsBin = "00000010";
  $fFlags = bytesToString(binaryToBytes($fFlagsBin));
  // chunk payload
  $fileContents .= $frameDataChunkSize.$fOrigin.$fSize.$fDuration.$fFlags.$frame['frameData'];
endforeach;

// calculate Size and build file header
$fileSize = bytesToString(toUint32(strlen($fileContents)+4));
$fileHeader = "RIFF".$fileSize."WEBP";
$fileWEBP = $fileHeader.$fileContents;

file_put_contents('animated.webp',$fileWEBP);

link of the repo in case you want to extend it or make a fork with the part of extracting the gif frames

https://github.com/aloMalbarez/php-GD-Animated-webp

to extract the gif frames you can use this class

https://github.com/jacoka/GIFDecoder

wave foreach ( $gifDecoder -> GIFGetFrames ( ) as $frame )

Edit I put a link to the GIFDecoder class of the original author

Scroll to Top