<?php

/**
 * Extract the frames (and their duration) of a GIF
 * 
 * @version 1.5
 * @link https://github.com/Sybio/GifFrameExtractor
 * @author Sybio (Clément Guillemain  / @Sybio01)
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 * @copyright Clément Guillemain
 */
class GifFrameExtractor
{
    
// Properties
    // ===================================================================================
    
    /**
     * @var resource
     */
    
private $gif;
    
    
/**
     * @var array
     */
    
private $frames;
    
    
/**
     * @var array
     */
    
private $frameDurations;
    
    
/**
     * @var array
     */
    
private $frameImages;
    
    
/**
     * @var array
     */
    
private $framePositions;
    
    
/**
     * @var array
     */
    
private $frameDimensions;
    
    
/**
     * @var integer 
     * 
     * (old: $this->index)
     */
    
private $frameNumber;
    
    
/**
     * @var array 
     * 
     * (old: $this->imagedata)
     */
    
private $frameSources;
    
    
/**
     * @var array 
     * 
     * (old: $this->fileHeader)
     */
    
private $fileHeader;
    
    
/**
     * @var integer The reader pointer in the file source 
     * 
     * (old: $this->pointer)
     */
    
private $pointer;
    
    
/**
     * @var integer
     */
    
private $gifMaxWidth;
    
    
/**
     * @var integer
     */
    
private $gifMaxHeight;
    
    
/**
     * @var integer
     */
    
private $totalDuration;
    
    
/**
     * @var integer
     */
    
private $handle;
    
    
/**
     * @var array 
     * 
     * (old: globaldata)
     */
    
private $globaldata;
    
    
/**
     * @var array 
     * 
     * (old: orgvars)
     */
    
private $orgvars;
    
    
// Methods
    // ===================================================================================
    
    /**
     * Extract frames of a GIF
     * 
     * @param string $filename GIF filename path
     * @param boolean $originalFrames Get original frames (with transparent background)
     * 
     * @return array
     */
    
public function extract($filename$originalFrames false)
    {
        if (!
self::isAnimatedGif($filename)) {
            throw new \
Exception('The GIF image you are trying to explode is not animated !');
        }
        
        
$this->reset();
        
$this->parseFramesInfo($filename);
        
$prevImg null;
        
        for (
$i 0$i count($this->frameSources); $i++) {
            
            
$this->frames[$i] = array();
            
$this->frameDurations[$i] = $this->frames[$i]['duration'] = $this->frameSources[$i]['delay_time'];
            
            
$img imagecreatefromstring($this->fileHeader["gifheader"].$this->frameSources[$i]["graphicsextension"].$this->frameSources[$i]["imagedata"].chr(0x3b));
            
            if (!
$originalFrames) {
                
                if (
$i 0) {
                    
                    
$prevImg $this->frames[$i 1]['image'];
                    
                } else {
                    
                    
$prevImg $img;
                }
                
                
$sprite imagecreate($this->gifMaxWidth$this->gifMaxHeight);
                
imagesavealpha($spritetrue);
                
                
$transparent imagecolortransparent($prevImg);
                
                if (
$transparent > -&& imagecolorstotal($prevImg) > $transparent) {
                    
                    
$actualTrans imagecolorsforindex($prevImg$transparent);
                    
imagecolortransparent($spriteimagecolorallocate($sprite$actualTrans['red'], $actualTrans['green'], $actualTrans['blue']));
                }
                
                if ((int) 
$this->frameSources[$i]['disposal_method'] == && $i 0) {
                    
                    
imagecopy($sprite$prevImg0000$this->gifMaxWidth$this->gifMaxHeight);
                }
                
                
imagecopyresampled($sprite$img$this->frameSources[$i]["offset_left"], $this->frameSources[$i]["offset_top"], 00$this->gifMaxWidth$this->gifMaxHeight$this->gifMaxWidth$this->gifMaxHeight);
                
$img $sprite;
            }
            
            
$this->frameImages[$i] = $this->frames[$i]['image'] = $img;
        }
        
        return 
$this->frames;
    }
    
    
/**
     * Check if a GIF file at a path is animated or not
     * 
     * @param string $filename GIF path
     */
    
public static function isAnimatedGif($filename)
    {
        if (!(
$fh = @fopen($filename'rb'))) {
            return 
false;
        }
        
        
$count 0;

        while (!
feof($fh) && $count 2) {
            
            
$chunk fread($fh1024 100); //read 100kb at a time
            
$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s'$chunk$matches);
        }
        
        
fclose($fh);
        return 
$count 1;
    }
    
    
// Internals
    // ===================================================================================
    
    /**
     * Parse the frame informations contained in the GIF file
     * 
     * @param string $filename GIF filename path
     */
    
private function parseFramesInfo($filename)
    {
        
$this->openFile($filename);
        
$this->parseGifHeader();
        
$this->parseGraphicsExtension(0);
        
$this->getApplicationData();
        
$this->getApplicationData();
        
$this->getFrameString(0);
        
$this->parseGraphicsExtension(1);
        
$this->getCommentData();
        
$this->getApplicationData();
        
$this->getFrameString(1);
        
        while (!
$this->checkByte(0x3b) && !$this->checkEOF()) {
            
            
$this->getCommentData(1);
            
$this->parseGraphicsExtension(2);
            
$this->getFrameString(2);
            
$this->getApplicationData();
        }
    }
    
    
/** 
     * Parse the gif header (old: get_gif_header)
     */
    
private function parseGifHeader()
    {
        
$this->pointerForward(10);
        
        if (
$this->readBits(($mybyte $this->readByteInt()), 01) == 1) {
            
            
$this->pointerForward(2);
            
$this->pointerForward(pow(2$this->readBits($mybyte53) + 1) * 3);
            
        } else {
            
            
$this->pointerForward(2);
        }

        
$this->fileHeader["gifheader"] = $this->dataPart(0$this->pointer);
        
        
// Decoding
        
$this->orgvars["gifheader"] = $this->fileHeader["gifheader"];
        
$this->orgvars["background_color"] = $this->orgvars["gifheader"][11];
    }
    
    
/**
     * Parse the application data of the frames (old: get_application_data)
     */
    
private function getApplicationData()
    {
        
$startdata $this->readByte(2);
        
        if (
$startdata == chr(0x21).chr(0xff)) {
            
            
$start $this->pointer 2;
            
$this->pointerForward($this->readByteInt());
            
$this->readDataStream($this->readByteInt());
            
$this->fileHeader["applicationdata"] = $this->dataPart($start$this->pointer $start);
            
        } else {
            
            
$this->pointerRewind(2);
        }
    }
    
    
/**
     * Parse the comment data of the frames (old: get_comment_data)
     */
    
private function getCommentData()
    {
        
$startdata $this->readByte(2);
        
        if (
$startdata == chr(0x21).chr(0xfe)) {
            
            
$start $this->pointer 2;
            
$this->readDataStream($this->readByteInt());
            
$this->fileHeader["commentdata"] = $this->dataPart($start$this->pointer $start);
            
        } else {
            
            
$this->pointerRewind(2);
        }
    }
    
    
/**
     * Parse the graphic extension of the frames (old: get_graphics_extension)
     * 
     * @param integer $type
     */
    
private function parseGraphicsExtension($type)
    {
        
$startdata $this->readByte(2);
        
        if (
$startdata == chr(0x21).chr(0xf9)) {
            
            
$start $this->pointer 2;
            
$this->pointerForward($this->readByteInt());
            
$this->pointerForward(1);
            
            if (
$type == 2) {
                
                
$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->dataPart($start$this->pointer $start);
                
            } elseif (
$type == 1) {
                
                
$this->orgvars["hasgx_type_1"] = 1;
                
$this->globaldata["graphicsextension"] = $this->dataPart($start$this->pointer $start);
                
            } elseif (
$type == 0) {
                
                
$this->orgvars["hasgx_type_0"] = 1;
                
$this->globaldata["graphicsextension_0"] = $this->dataPart($start$this->pointer $start);
            }
            
        } else {
            
            
$this->pointerRewind(2);
        }
    }
    
    
/**
     * Get the full frame string block (old: get_image_block)
     * 
     * @param integer $type
     */
    
private function getFrameString($type)
    {
        if (
$this->checkByte(0x2c)) {
            
            
$start $this->pointer;
            
$this->pointerForward(9);
            
            if (
$this->readBits(($mybyte $this->readByteInt()), 01) == 1) {
                
                
$this->pointerForward(pow(2$this->readBits($mybyte53) + 1) * 3);
            }
            
            
$this->pointerForward(1);
            
$this->readDataStream($this->readByteInt());
            
$this->frameSources[$this->frameNumber]["imagedata"] = $this->dataPart($start$this->pointer $start);

            if (
$type == 0) {
                
                
$this->orgvars["hasgx_type_0"] = 0;
                
                if (isset(
$this->globaldata["graphicsextension_0"])) {
                    
                    
$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->globaldata["graphicsextension_0"];
                    
                } else {
                    
                    
$this->frameSources[$this->frameNumber]["graphicsextension"] = null;
                }
                    
                unset(
$this->globaldata["graphicsextension_0"]);
                
            } elseif (
$type == 1) {
                
                if (isset(
$this->orgvars["hasgx_type_1"]) && $this->orgvars["hasgx_type_1"] == 1) {
                    
                    
$this->orgvars["hasgx_type_1"] = 0;
                    
$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->globaldata["graphicsextension"];
                    unset(
$this->globaldata["graphicsextension"]);
                    
                } else {
                    
                    
$this->orgvars["hasgx_type_0"] = 0;
                    
$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->globaldata["graphicsextension_0"];
                    unset(
$this->globaldata["graphicsextension_0"]);
                }
            }

            
$this->parseFrameData();
            
$this->frameNumber++;
        }
    }
    
    
/**
     * Parse frame data string into an array (old: parse_image_data)
     */
    
private function parseFrameData()
    {
        
$this->frameSources[$this->frameNumber]["disposal_method"] = $this->getImageDataBit("ext"333);
        
$this->frameSources[$this->frameNumber]["user_input_flag"] = $this->getImageDataBit("ext"361);
        
$this->frameSources[$this->frameNumber]["transparent_color_flag"] = $this->getImageDataBit("ext"371);
        
$this->frameSources[$this->frameNumber]["delay_time"] = $this->dualByteVal($this->getImageDataByte("ext"42));
        
$this->totalDuration += (int) $this->frameSources[$this->frameNumber]["delay_time"];
        
$this->frameSources[$this->frameNumber]["transparent_color_index"] = ord($this->getImageDataByte("ext"61));
        
$this->frameSources[$this->frameNumber]["offset_left"] = $this->dualByteVal($this->getImageDataByte("dat"12));
        
$this->frameSources[$this->frameNumber]["offset_top"] = $this->dualByteVal($this->getImageDataByte("dat"32));
        
$this->frameSources[$this->frameNumber]["width"] = $this->dualByteVal($this->getImageDataByte("dat"52));
        
$this->frameSources[$this->frameNumber]["height"] = $this->dualByteVal($this->getImageDataByte("dat"72));
        
$this->frameSources[$this->frameNumber]["local_color_table_flag"] = $this->getImageDataBit("dat"901);
        
$this->frameSources[$this->frameNumber]["interlace_flag"] = $this->getImageDataBit("dat"911);
        
$this->frameSources[$this->frameNumber]["sort_flag"] = $this->getImageDataBit("dat"921);
        
$this->frameSources[$this->frameNumber]["color_table_size"] = pow(2$this->getImageDataBit("dat"953) + 1) * 3;
        
$this->frameSources[$this->frameNumber]["color_table"] = substr($this->frameSources[$this->frameNumber]["imagedata"], 10$this->frameSources[$this->frameNumber]["color_table_size"]);
        
$this->frameSources[$this->frameNumber]["lzw_code_size"] = ord($this->getImageDataByte("dat"101));
        
        
$this->framePositions[$this->frameNumber] = array(
            
'x' => $this->frameSources[$this->frameNumber]["offset_left"],
            
'y' => $this->frameSources[$this->frameNumber]["offset_top"],
        );
        
        
$this->frameDimensions[$this->frameNumber] = array(
            
'width' => $this->frameSources[$this->frameNumber]["width"],
            
'height' => $this->frameSources[$this->frameNumber]["height"],
        );
                
        
// Decoding
        
$this->orgvars[$this->frameNumber]["transparent_color_flag"] = $this->frameSources[$this->frameNumber]["transparent_color_flag"];
        
$this->orgvars[$this->frameNumber]["transparent_color_index"] = $this->frameSources[$this->frameNumber]["transparent_color_index"];
        
$this->orgvars[$this->frameNumber]["delay_time"] = $this->frameSources[$this->frameNumber]["delay_time"];
        
$this->orgvars[$this->frameNumber]["disposal_method"] = $this->frameSources[$this->frameNumber]["disposal_method"];
        
$this->orgvars[$this->frameNumber]["offset_left"] = $this->frameSources[$this->frameNumber]["offset_left"];
        
$this->orgvars[$this->frameNumber]["offset_top"] = $this->frameSources[$this->frameNumber]["offset_top"];
        
        
// Updating the max width
        
if ($this->gifMaxWidth $this->frameSources[$this->frameNumber]["width"]) {
            
            
$this->gifMaxWidth $this->frameSources[$this->frameNumber]["width"];
        }
        
        
// Updating the max height
        
if ($this->gifMaxHeight $this->frameSources[$this->frameNumber]["height"]) {
            
            
$this->gifMaxHeight $this->frameSources[$this->frameNumber]["height"];
        }
    }
    
    
/**
     * Get the image data byte (old: get_imagedata_byte)
     * 
     * @param string $type
     * @param integer $start
     * @param integer $length
     * 
     * @return string
     */
    
private function getImageDataByte($type$start$length)
    {
        if (
$type == "ext") {
            
            return 
substr($this->frameSources[$this->frameNumber]["graphicsextension"], $start$length);
        }
        
        
// "dat"
        
return substr($this->frameSources[$this->frameNumber]["imagedata"], $start$length);
    }
    
    
/**
     * Get the image data bit (old: get_imagedata_bit)
     * 
     * @param string $type
     * @param integer $byteIndex
     * @param integer $bitStart
     * @param integer $bitLength
     * 
     * @return number
     */
    
private function getImageDataBit($type$byteIndex$bitStart$bitLength)
    {
        if (
$type == "ext") {
            
            return 
$this->readBits(ord(substr($this->frameSources[$this->frameNumber]["graphicsextension"], $byteIndex1)), $bitStart$bitLength);
        } 
        
        
// "dat"
        
return $this->readBits(ord(substr($this->frameSources[$this->frameNumber]["imagedata"], $byteIndex1)), $bitStart$bitLength);
    }
    
    
/**
     * Return the value of 2 ASCII chars (old: dualbyteval)
     * 
     * @param string $s
     * 
     * @return integer
     */
    
private function dualByteVal($s)
    {
        
$i ord($s[1]) * 256 ord($s[0]);
        
        return 
$i;
    }
    
    
/**
     * Read the data stream (old: read_data_stream)
     * 
     * @param integer $firstLength
     */
    
private function readDataStream($firstLength)
    {
        
$this->pointerForward($firstLength);
        
$length $this->readByteInt();
        
        if (
$length != 0) {
            
            while (
$length != 0) {
                
                
$this->pointerForward($length);
                
$length $this->readByteInt();
            }
        }
    }
    
    
/**
     * Open the gif file (old: loadfile)
     * 
     * @param string $filename
     */
    
private function openFile($filename)
    {
        
$this->handle fopen($filename"rb");
        
$this->pointer 0;
        
        
$imageSize getimagesize($filename);
        
$this->gifWidth $imageSize[0];
        
$this->gifHeight $imageSize[1];
    }
    
    
/**
     * Close the read gif file (old: closefile)
     */
    
private function closeFile()
    {
        
fclose($this->handle);
        
$this->handle 0;
    }
    
    
/**
     * Read the file from the beginning to $byteCount in binary (old: readbyte)
     * 
     * @param integer $byteCount
     * 
     * @return string
     */
    
private function readByte($byteCount)
    {
        
$data fread($this->handle$byteCount);
        
$this->pointer += $byteCount;
        
        return 
$data;
    }
    
    
/**
     * Read a byte and return ASCII value (old: readbyte_int)
     * 
     * @return integer
     */
    
private function readByteInt()
    {
        
$data fread($this->handle1);
        
$this->pointer++;
        
        return 
ord($data);
    }
    
    
/**
     * Convert a $byte to decimal (old: readbits)
     * 
     * @param string $byte
     * @param integer $start
     * @param integer $length
     * 
     * @return number
     */
    
private function readBits($byte$start$length)
    {
        
$bin str_pad(decbin($byte), 8"0"STR_PAD_LEFT);
        
$data substr($bin$start$length);
        
        return 
bindec($data);
    }
    
    
/**
     * Rewind the file pointer reader (old: p_rewind)
     * 
     * @param integer $length
     */
    
private function pointerRewind($length)
    {
        
$this->pointer -= $length;
        
fseek($this->handle$this->pointer);
    }
    
    
/**
     * Forward the file pointer reader (old: p_forward)
     * 
     * @param integer $length
     */
    
private function pointerForward($length)
    {
        
$this->pointer += $length;
        
fseek($this->handle$this->pointer);
    }
    
    
/**
     * Get a section of the data from $start to $start + $length (old: datapart)
     * 
     * @param integer $start
     * @param integer $length
     * 
     * @return string
     */
    
private function dataPart($start$length)
    {
        
fseek($this->handle$start);
        
$data fread($this->handle$length);
        
fseek($this->handle$this->pointer);
        
        return 
$data;
    }
    
    
/**
     * Check if a character if a byte (old: checkbyte)
     * 
     * @param integer $byte
     * 
     * @return boolean
     */
    
private function checkByte($byte)
    {
        if (
fgetc($this->handle) == chr($byte)) {
            
            
fseek($this->handle$this->pointer);
            return 
true;
        }
        
        
fseek($this->handle$this->pointer);

        return 
false;
    }
    
    
/**
     * Check the end of the file (old: checkEOF)
     * 
     * @return boolean
     */
    
private function checkEOF()
    {
        if (
fgetc($this->handle) === false) {
            
            return 
true;
        }
            
        
fseek($this->handle$this->pointer);
        
        return 
false;
    }
    
    
/**
     * Reset and clear this current object
     */
    
private function reset()
    {
        
$this->gif null;
        
$this->totalDuration $this->gifMaxHeight $this->gifMaxWidth $this->handle $this->pointer $this->frameNumber 0;
        
$this->frameDimensions $this->framePositions $this->frameImages $this->frameDurations $this->globaldata $this->orgvars $this->frames $this->fileHeader $this->frameSources = array();
    }
    
    
// Getter / Setter
    // ===================================================================================
    
    /**
     * Get the total of all added frame duration
     * 
     * @return integer
     */
    
public function getTotalDuration()
    {
        return 
$this->totalDuration;
    }
    
    
/**
     * Get the number of extracted frames
     * 
     * @return integer
     */
    
public function getFrameNumber()
    {
        return 
$this->frameNumber;
    }
    
    
/**
     * Get the extracted frames (images and durations)
     * 
     * @return array
     */
    
public function getFrames()
    {
        return 
$this->frames;
    }
    
    
/**
     * Get the extracted frame positions
     * 
     * @return array
     */
    
public function getFramePositions()
    {
        return 
$this->framePositions;
    }
    
    
/**
     * Get the extracted frame dimensions
     * 
     * @return array
     */
    
public function getFrameDimensions()
    {
        return 
$this->frameDimensions;
    }
    
    
/**
     * Get the extracted frame images
     * 
     * @return array
     */
    
public function getFrameImages()
    {
        return 
$this->frameImages;
    }
    
    
/**
     * Get the extracted frame durations
     * 
     * @return array
     */
    
public function getFrameDurations()
    {
        return 
$this->frameDurations;
    }
}