diff --git a/README.md b/README.md index 847eb0d..7c44bc7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +> *NOTE: This is a fork of [Clément Guillemain](https://github.com/Sybio)'s nice [GifCreator class](https://github.com/Sybio/GifCreator), with some minor bug fixes and most pull requests merged together. + # ================================ # GifCreator # ================================ @@ -13,7 +15,7 @@ This class helps you to create an animated GIF image: give multiple images and t **1 - Creation:** ```php -// Create an array containing file paths, resource var (initialized with imagecreatefromXXX), +// Create an array containing file paths, resource var (initialized with imagecreatefromXXX), // image URLs or even binary code from image files. // All sorted in order to appear. $frames = array( @@ -23,11 +25,11 @@ $frames = array( 'http://thisisafakedomain.com/images/pic4.jpg', // URL ); -// Create an array containing the duration (in millisecond) of each frames (in order too) +// Create an array containing the duration (in centiseconds (hundredths of a second)) of each frames (in order too) $durations = array(40, 80, 40, 20); // Initialize and create the GIF ! -$gc = new GifCreator(); +$gc = new GifCreator\GifCreator(); $gc->create($frames, $durations, 5); ``` The 3rd parameter of create() method allows you to choose the number of loop of your animated gif before it stops. @@ -59,9 +61,9 @@ file_put_contents('/myfolder/animated_picture.gif', $gifBinary); ### Behavior - The transparency is based on the first given frame. It will be saved only if you give multiple frames with same transparent background. -- The dimensions of the generated GIF are based on the first frame. If you need to resize your frames to get the same dimension, you can use +- The dimensions of the generated GIF are based on the first frame. If you need to resize your frames to get the same dimension, you can use this class: https://github.com/Sybio/ImageWorkshop ### About -The class reuses some part of code of "GIFEncoder.class.php" by László Zsidi (thanks to him). \ No newline at end of file +The class reuses some part of code of "GIFEncoder.class.php" by László Zsidi (thanks to him). diff --git a/composer.json b/composer.json index 9d98c23..02e7fce 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "sybio/gif-creator", + "name": "moroz1999/gif-creator", "type": "library", "description": "PHP class that creates animated GIF from multiple images", "keywords": ["gif", "animated", "creation", "encode", "PHP"], @@ -13,9 +13,10 @@ } ], "require": { - "php": ">=5.3.0" + "php": "^8.0", + "ext-gd": "*" }, "autoload": { "psr-0": { "GifCreator": "src" } } -} \ No newline at end of file +} diff --git a/src/GifCreator/GifCreator.php b/src/GifCreator/GifCreator.php index 0726243..8d2e5ff 100644 --- a/src/GifCreator/GifCreator.php +++ b/src/GifCreator/GifCreator.php @@ -4,7 +4,7 @@ /** * Create an animated GIF from multiple images - * + * * @version 1.0 * @link https://github.com/Sybio/GifCreator * @author Sybio (Clément Guillemain / @Sybio01) @@ -17,12 +17,12 @@ class GifCreator * @var string The gif string source (old: this->GIF) */ private $gif; - + /** * @var string Encoder version (old: this->VER) */ - private $version; - + private $version; + /** * @var boolean Check the image is build or not (old: this->IMG) */ @@ -31,327 +31,333 @@ class GifCreator /** * @var array Frames string sources (old: this->BUF) */ - private $frameSources; - + private $frameSources; + /** * @var integer Gif loop (old: this->LOP) */ - private $loop; - + private $loop; + /** * @var integer Gif dis (old: this->DIS) */ - private $dis; - + private $dis; + /** * @var integer Gif color (old: this->COL) */ - private $colour; - + private $colour; + /** * @var array (old: this->ERR) */ - private $errors; - + private $errors; + // Methods // =================================================================================== - + /** * Constructor */ public function __construct() { $this->reset(); - + // Static data $this->version = 'GifCreator: Under development'; $this->errors = array( - 'ERR00' => 'Does not supported function for only one image.', - 'ERR01' => 'Source is not a GIF image.', - 'ERR02' => 'You have to give resource image variables, image URL or image binary sources in $frames array.', - 'ERR03' => 'Does not make animation from animated GIF source.', + 'ERR00' => 'Does not support single image/duration input.', + 'ERR01' => 'Source is not a GIF image.', + 'ERR02' => 'You have to give resource image variables, image URL or image binary sources in $frames array.', + 'ERR03' => 'Does not make animation from animated GIF source.', ); } - /** + /** * Create the GIF string (old: GIFEncoder) - * + * * @param array $frames An array of frame: can be file paths, resource image variables, binary sources or image URLs - * @param array $durations An array containing the duration of each frame + * @param array $durations An array containing the duration of each frame (in centiseconds) * @param integer $loop Number of GIF loops before stopping animation (Set 0 to get an infinite loop) - * * @return string The GIF string source + * @throws \Exception */ - public function create($frames = array(), $durations = array(), $loop = 0) + public function create($frames = array(), $durations = array(), $loop = 0) { - if (!is_array($frames) && !is_array($GIF_tim)) { - - throw new \Exception($this->version.': '.$this->errors['ERR00']); - } - - $this->loop = ($loop > -1) ? $loop : 0; - $this->dis = 2; - - for ($i = 0; $i < count($frames); $i++) { - - if (is_resource($frames[$i])) { // Resource var - + if (!is_array($frames) && !is_array($durations)) { + + throw new \Exception($this->version . ': ' . $this->errors['ERR00']); + } + + $this->loop = ($loop > -1) ? $loop : 0; + $this->dis = 2; + + for ($i = 0; $i < count($frames); $i++) { + + if (is_resource($frames[$i])) { // Resource var + $resourceImg = $frames[$i]; - + ob_start(); imagegif($frames[$i]); $this->frameSources[] = ob_get_contents(); ob_end_clean(); - + } elseif (is_string($frames[$i])) { // File path or URL or Binary source code - - if (file_exists($frames[$i]) || filter_var($frames[$i], FILTER_VALIDATE_URL)) { // File path - - $frames[$i] = file_get_contents($frames[$i]); + + if ((substr($frames[$i], 0, 4) != 'GIF8') && file_exists($frames[$i]) || filter_var($frames[$i], FILTER_VALIDATE_URL)) { // File path + + $frames[$i] = file_get_contents($frames[$i]); } - + $resourceImg = imagecreatefromstring($frames[$i]); - + ob_start(); imagegif($resourceImg); $this->frameSources[] = ob_get_contents(); ob_end_clean(); - - } else { // Fail - - throw new \Exception($this->version.': '.$this->errors['ERR02'].' ('.$mode.')'); - } - + + } else { // Fail + + throw new \Exception($this->version . ': ' . $this->errors['ERR02']);//.' ('.$mode.')'? + } + if ($i == 0) { - + $colour = imagecolortransparent($resourceImg); } - - if (substr($this->frameSources[$i], 0, 6) != 'GIF87a' && substr($this->frameSources[$i], 0, 6) != 'GIF89a') { - - throw new \Exception($this->version.': '.$i.' '.$this->errors['ERR01']); - } - - for ($j = (13 + 3 * (2 << (ord($this->frameSources[$i] { 10 }) & 0x07))), $k = TRUE; $k; $j++) { - - switch ($this->frameSources[$i] { $j }) { - - case '!': - - if ((substr($this->frameSources[$i], ($j + 3), 8)) == 'NETSCAPE') { - - throw new \Exception($this->version.': '.$this->errors['ERR03'].' ('.($i + 1).' source).'); - } - - break; - - case ';': - - $k = false; - break; - } - } - + + if (substr($this->frameSources[$i], 0, 6) != 'GIF87a' && substr($this->frameSources[$i], 0, 6) != 'GIF89a') { + + throw new \Exception($this->version . ': ' . $i . ' ' . $this->errors['ERR01']); + } + + for ($j = (13 + 3 * (2 << (ord($this->frameSources[$i][10]) & 0x07))), $k = true; $k; $j++) { + + switch ($this->frameSources[$i][$j]) { + + case '!': + + if ((substr($this->frameSources[$i], ($j + 3), 8)) == 'NETSCAPE') { + + throw new \Exception($this->version . ': ' . $this->errors['ERR03'] . ' (' . ($i + 1) . ' source).'); + } + + break; + + case ';': + + $k = false; + break; + } + } + unset($resourceImg); - } - + } + if (isset($colour)) { - + $this->colour = $colour; - + } else { - + $red = $green = $blue = 0; $this->colour = ($red > -1 && $green > -1 && $blue > -1) ? ($red | ($green << 8) | ($blue << 16)) : -1; } - - $this->gifAddHeader(); - - for ($i = 0; $i < count($this->frameSources); $i++) { - - $this->addGifFrames($i, $durations[$i]); - } - - $this->gifAddFooter(); - + + $this->gifAddHeader(); + + for ($i = 0; $i < count($this->frameSources); $i++) { + + $this->addGifFrames($i, $durations[$i]); + } + + $this->gifAddFooter(); + return $this->gif; - } - + } + // Internals // =================================================================================== - - /** + + /** * Add the header gif string in its source (old: GIFAddHeader) */ - public function gifAddHeader() + public function gifAddHeader() { - $cmap = 0; - - if (ord($this->frameSources[0] { 10 }) & 0x80) { - - $cmap = 3 * (2 << (ord($this->frameSources[0] { 10 }) & 0x07)); - - $this->gif .= substr($this->frameSources[0], 6, 7); - $this->gif .= substr($this->frameSources[0], 13, $cmap); - $this->gif .= "!\377\13NETSCAPE2.0\3\1".$this->encodeAsciiToChar($this->loop)."\0"; - } - } - - /** + if (ord($this->frameSources[0][10]) & 0x80) { + + $cmap = 3 * (2 << (ord($this->frameSources[0][10]) & 0x07)); + + $this->gif .= substr($this->frameSources[0], 6, 7); + $this->gif .= substr($this->frameSources[0], 13, $cmap); + if ($this->loop != 1) { + // Browsers interpret loop count differently, but for loop == 1 + // we can work around this by excluding loop count completely + // since all browsers seem to handle "no loop count" as + // "play once only". + $this->gif .= "!\377\13NETSCAPE2.0\3\1" . $this->encodeAsciiToChar($this->loop) . "\0"; + } + } + } + + /** * Add the frame sources to the GIF string (old: GIFAddFrames) - * + * * @param integer $i * @param integer $d */ - public function addGifFrames($i, $d) + public function addGifFrames($i, $d) { - $Locals_str = 13 + 3 * (2 << (ord($this->frameSources[ $i ] { 10 }) & 0x07)); - - $Locals_end = strlen($this->frameSources[$i]) - $Locals_str - 1; - $Locals_tmp = substr($this->frameSources[$i], $Locals_str, $Locals_end); - - $Global_len = 2 << (ord($this->frameSources[0 ] { 10 }) & 0x07); - $Locals_len = 2 << (ord($this->frameSources[$i] { 10 }) & 0x07); - - $Global_rgb = substr($this->frameSources[0], 13, 3 * (2 << (ord($this->frameSources[0] { 10 }) & 0x07))); - $Locals_rgb = substr($this->frameSources[$i], 13, 3 * (2 << (ord($this->frameSources[$i] { 10 }) & 0x07))); - - $Locals_ext = "!\xF9\x04".chr(($this->dis << 2) + 0).chr(($d >> 0 ) & 0xFF).chr(($d >> 8) & 0xFF)."\x0\x0"; - - if ($this->colour > -1 && ord($this->frameSources[$i] { 10 }) & 0x80) { - - for ($j = 0; $j < (2 << (ord($this->frameSources[$i] { 10 } ) & 0x07)); $j++) { - - if (ord($Locals_rgb { 3 * $j + 0 }) == (($this->colour >> 16) & 0xFF) && - ord($Locals_rgb { 3 * $j + 1 }) == (($this->colour >> 8) & 0xFF) && - ord($Locals_rgb { 3 * $j + 2 }) == (($this->colour >> 0) & 0xFF) - ) { - $Locals_ext = "!\xF9\x04".chr(($this->dis << 2) + 1).chr(($d >> 0) & 0xFF).chr(($d >> 8) & 0xFF).chr($j)."\x0"; - break; - } - } - } - - switch ($Locals_tmp { 0 }) { - - case '!': - - $Locals_img = substr($Locals_tmp, 8, 10); - $Locals_tmp = substr($Locals_tmp, 18, strlen($Locals_tmp) - 18); - - break; - - case ',': - - $Locals_img = substr($Locals_tmp, 0, 10); - $Locals_tmp = substr($Locals_tmp, 10, strlen($Locals_tmp) - 10); - - break; - } - - if (ord($this->frameSources[$i] { 10 }) & 0x80 && $this->imgBuilt) { - - if ($Global_len == $Locals_len) { - - if ($this->gifBlockCompare($Global_rgb, $Locals_rgb, $Global_len)) { - - $this->gif .= $Locals_ext.$Locals_img.$Locals_tmp; - - } else { - - $byte = ord($Locals_img { 9 }); - $byte |= 0x80; - $byte &= 0xF8; - $byte |= (ord($this->frameSources[0] { 10 }) & 0x07); - $Locals_img { 9 } = chr($byte); - $this->gif .= $Locals_ext.$Locals_img.$Locals_rgb.$Locals_tmp; - } - - } else { - - $byte = ord($Locals_img { 9 }); - $byte |= 0x80; - $byte &= 0xF8; - $byte |= (ord($this->frameSources[$i] { 10 }) & 0x07); - $Locals_img { 9 } = chr($byte); - $this->gif .= $Locals_ext.$Locals_img.$Locals_rgb.$Locals_tmp; - } - - } else { - - $this->gif .= $Locals_ext.$Locals_img.$Locals_tmp; - } - - $this->imgBuilt = true; - } - - /** + $Locals_str = 13 + 3 * (2 << (ord($this->frameSources[$i][10]) & 0x07)); + + $Locals_end = strlen($this->frameSources[$i]) - $Locals_str - 1; + $Locals_tmp = substr($this->frameSources[$i], $Locals_str, $Locals_end); + + $Global_len = 2 << (ord($this->frameSources[0][10]) & 0x07); + $Locals_len = 2 << (ord($this->frameSources[$i][10]) & 0x07); + + $Global_rgb = substr($this->frameSources[0], 13, 3 * (2 << (ord($this->frameSources[0][10]) & 0x07))); + $Locals_rgb = substr($this->frameSources[$i], 13, 3 * (2 << (ord($this->frameSources[$i][10]) & 0x07))); + + $Locals_ext = "!\xF9\x04" . chr(($this->dis << 2) + 0) . chr(($d >> 0) & 0xFF) . chr(($d >> 8) & 0xFF) . "\x0\x0"; + + if ($this->colour > -1 && ord($this->frameSources[$i][10]) & 0x80) { + + for ($j = 0; $j < (2 << (ord($this->frameSources[$i][10]) & 0x07)); $j++) { + + if (ord($Locals_rgb[3 * $j + 0]) == (($this->colour >> 16) & 0xFF) && + ord($Locals_rgb[3 * $j + 1]) == (($this->colour >> 8) & 0xFF) && + ord($Locals_rgb[3 * $j + 2]) == (($this->colour >> 0) & 0xFF) + ) { + $Locals_ext = "!\xF9\x04" . chr(($this->dis << 2) + 1) . chr(($d >> 0) & 0xFF) . chr(($d >> 8) & 0xFF) . chr($j) . "\x0"; + break; + } + } + } + $Locals_img = ''; + + switch ($Locals_tmp[0]) { + + case '!': + + $Locals_img = substr($Locals_tmp, 8, 10); + $Locals_tmp = substr($Locals_tmp, 18, strlen($Locals_tmp) - 18); + + break; + + case ',': + + $Locals_img = substr($Locals_tmp, 0, 10); + $Locals_tmp = substr($Locals_tmp, 10, strlen($Locals_tmp) - 10); + + break; + } + if (ord($this->frameSources[$i][10]) & 0x80 && $this->imgBuilt) { + + if ($Global_len == $Locals_len) { + + if ($this->gifBlockCompare($Global_rgb, $Locals_rgb, $Global_len)) { + + $this->gif .= $Locals_ext . $Locals_img . $Locals_tmp; + + } else { + + $byte = ord($Locals_img[9]); + $byte |= 0x80; + $byte &= 0xF8; + $byte |= (ord($this->frameSources[0][10]) & 0x07); + $Locals_img[9] = chr($byte); + $this->gif .= $Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp; + } + + } else { + + $byte = ord($Locals_img[9]); + $byte |= 0x80; + $byte &= 0xF8; + $byte |= (ord($this->frameSources[$i][10]) & 0x07); + $Locals_img[9] = chr($byte); + $this->gif .= $Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp; + } + + } else { + + $this->gif .= $Locals_ext . $Locals_img . $Locals_tmp; + } + + $this->imgBuilt = true; + } + + /** * Add the gif string footer char (old: GIFAddFooter) */ - public function gifAddFooter() + public function gifAddFooter() { - $this->gif .= ';'; - } - - /** + $this->gif .= ';'; + } + + /** * Compare two block and return the version (old: GIFBlockCompare) - * + * * @param string $globalBlock * @param string $localBlock * @param integer $length - * + * * @return integer - */ - public function gifBlockCompare($globalBlock, $localBlock, $length) + */ + public function gifBlockCompare($globalBlock, $localBlock, $length) { - for ($i = 0; $i < $length; $i++) { - - if ($globalBlock { 3 * $i + 0 } != $localBlock { 3 * $i + 0 } || - $globalBlock { 3 * $i + 1 } != $localBlock { 3 * $i + 1 } || - $globalBlock { 3 * $i + 2 } != $localBlock { 3 * $i + 2 }) { - + for ($i = 0; $i < $length; $i++) { + + if ($globalBlock[3 * $i + 0] != $localBlock[3 * $i + 0] || + $globalBlock[3 * $i + 1] != $localBlock[3 * $i + 1] || + $globalBlock[3 * $i + 2] != $localBlock[3 * $i + 2] + ) { + return 0; - } - } + } + } + + return 1; + } - return 1; - } - - /** + /** * Encode an ASCII char into a string char (old: GIFWord) - * + * * $param integer $char ASCII char - * + * + * @param $char * @return string - */ - public function encodeAsciiToChar($char) + */ + public function encodeAsciiToChar($char) { - return (chr($char & 0xFF).chr(($char >> 8) & 0xFF)); - } - + return (chr($char & 0xFF) . chr(($char >> 8) & 0xFF)); + } + /** * Reset and clean the current object */ public function reset() { - $this->frameSources; + $this->frameSources = []; $this->gif = 'GIF89a'; // the GIF header $this->imgBuilt = false; $this->loop = 0; $this->dis = 2; $this->colour = -1; } - + // Getter / Setter // =================================================================================== - - /** + + /** * Get the final GIF image string (old: GetAnimation) - * + * * @return string - */ - public function getGif() + */ + public function getGif() { - return $this->gif; - } + return $this->gif; + } } \ No newline at end of file diff --git a/test/1.png b/test/1.png new file mode 100644 index 0000000..fd52cea Binary files /dev/null and b/test/1.png differ diff --git a/test/2.png b/test/2.png new file mode 100644 index 0000000..b879751 Binary files /dev/null and b/test/2.png differ diff --git a/test/index.php b/test/index.php new file mode 100644 index 0000000..81c3368 --- /dev/null +++ b/test/index.php @@ -0,0 +1,33 @@ +create($frames, $durations, 5); + + + $gifBinary = $gc->getGif(); + + if (isset($_GET['save'])) { + file_put_contents('final.gif', $gifBinary); + }else{ + header('Content-type: image/gif'); + header('Content-Disposition: filename="butterfly.gif"'); + echo $gifBinary; + exit; + } +