<?php
namespace Grafika\Gd\Helper;
final class GifHelper {
public function open($imageFile){
$fp = fopen( $imageFile, 'rb');
if($fp === false ) {
throw new \Exception(sprintf('Error loading file: "%s".', $imageFile));
}
$size = filesize( $imageFile );
$bytes = fread($fp, $size);
$bytes = unpack('H*', $bytes); $bytes = $bytes[1];
fclose($fp);
return new GifByteStream($bytes);
}
public function load($bin){
$bytes = unpack('H*', $bin); $bytes = $bytes[1];
return new GifByteStream($bytes);
}
public function isAnimated($bytes){
$bytes->setPosition(13);
$lastPos = $bytes->getPosition();
$gceCount = 0;
while (($lastPos = $bytes->find('21f904', $lastPos))!== false) {
$gceCount++;
if($gceCount>1){
return true;
}
}
return false;
}
public function encode($data){
$hex = '';
$hex .= $this->_fixSize($this->_asciiToHex($data['signature']),3);
$hex .= $this->_fixSize($this->_asciiToHex($data['version']),3);
$hex .= $this->_switchEndian($this->_fixSize(dechex($data['canvasWidth']), 4));
$hex .= $this->_switchEndian($this->_fixSize(dechex($data['canvasHeight']), 4));
$packedField = decbin($data['globalColorTableFlag']);
$packedField .= $this->_fixSize(decbin($data['colorResolution']), 3);
$packedField .= decbin($data['sortFlag']);
$packedField .= $this->_fixSize(decbin($data['sizeOfGlobalColorTable']), 3);
$hex .= $this->_fixSize(dechex(bindec($packedField)), 2);
$hex .= $this->_fixSize(dechex($data['backgroundColorIndex']), 2);
$hex .= $this->_fixSize(dechex($data['pixelAspectRatio']), 2);
if($data['globalColorTableFlag']>0) {
$hex .= $data['globalColorTable'];
}
if(isset($data['applicationExtension'])){
foreach($data['applicationExtension'] as $app){
$hex .= '21ff0b';
$hex .= $this->_fixSize($this->_asciiToHex($app['appId']),8);
$hex .= $this->_fixSize($this->_asciiToHex($app['appCode']),3);
foreach($app['subBlocks'] as $subBlock){
$len = $this->_fixSize(dechex(strlen($subBlock)/2),2);
$hex .= $len.$subBlock;
}
$hex .= '00';
}
}
foreach($data['frames'] as $i=>$frame){
if(isset($frame['delayTime'])) {
$hex .= '21f904';
$packedField = '000'; $packedField .= $this->_fixSize(decbin($frame['disposalMethod']), 3);
$packedField .= decbin($frame['userInputFlag']);
$packedField .= decbin($frame['transparentColorFlag']);
$hex .= $this->_fixSize(dechex(bindec($packedField)), 2);
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['delayTime']), 4));
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['transparentColorIndex']), 2));
$hex .= '00';
}
$hex .= '2c';
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['imageLeft']), 4));
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['imageTop']), 4));
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['imageWidth']), 4));
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['imageHeight']), 4));
$packedField = decbin($frame['localColorTableFlag']);
$packedField .= decbin($frame['interlaceFlag']);
$packedField .= decbin($frame['sortFlag']);
$packedField .= '00'; $packedField .= $this->_fixSize(decbin($frame['sizeOfLocalColorTable']), 3);
$hex .= $this->_fixSize(dechex(bindec($packedField)), 2);
if($frame['localColorTableFlag']>0){
$hex .= $frame['localColorTable'];
}
$hex .= $frame['imageData'];
}
$hex .= $data['trailer'];
return $hex;
}
public function decode($bytes){
$bytes->setPosition(0);
$blocks = $this->decodeToBlocks($bytes);
return $this->expandBlocks($blocks);
}
public function decodeToBlocks($bytes){
$bytes->setPosition(0);
$blocks = array();
$blocks['header'] = $bytes->bite(6);
$part = $bytes->bite(2); $hex = $part;
$part = $bytes->bite(2); $hex .= $part;
$part = $bytes->bite(1); $hex .= $part;
$bin = $this->_fixSize($this->_hexToBin($part),8);
$globalColorTableFlag = bindec(substr($bin, 0 ,1));
$sizeOfGlobalColorTable = bindec(substr($bin, 5 ,3));
$part = $bytes->bite(1); $hex .= $part;
$part = $bytes->bite(1); $hex .= $part;
$blocks['logicalScreenDescriptor'] = $hex;
if($globalColorTableFlag > 0){
$colorTableLength = 3*(pow(2,($sizeOfGlobalColorTable+1)));
$part = $bytes->bite($colorTableLength);
$blocks['globalColorTable'] = $part;
}
$commentC = $plainTextC = $appCount = $gce = $dc = 0; while(!$bytes->isEnd()){
$part = $bytes->bite(1);
if('21'===$part){ $hex = $part;
$part = $bytes->bite(1);
if('ff'===$part) { $hex .= $part;
$part = $bytes->bite(1); $size = hexdec($part); $hex .= $part;
$part = $bytes->bite($size); $hex .= $part;
while (!$bytes->isEnd()) { $nextSize = $bytes->bite(1);
if($nextSize !== '00'){
$hex .= $nextSize;
$size = hexdec($nextSize);
$part = $bytes->bite($size);
$hex .= $part;
} else {
$hex .= $nextSize;
$blocks['applicationExtension-'.$appCount] = $hex;
break;
}
}
$appCount++;
} else if('f9'===$part){ $hex .= $part;
$part = $bytes->bite(1); $hex .= $part;
$part = $bytes->bite(1); $hex .= $part;
$part = $bytes->bite(2); $hex .= $part;
$part = $bytes->bite(1); $hex .= $part;
$part = $bytes->bite(1); $hex .= $part;
$blocks['graphicControlExtension-'.$gce] = $hex;
$gce++;
} else if('01' === $part){ $hex .= $part;
while (!$bytes->isEnd()) { $nextSize = $bytes->bite(1);
if($nextSize !== '00'){
$hex .= $nextSize;
$size = hexdec($nextSize);
$part = $bytes->bite($size);
$hex .= $part;
} else {
$hex .= $nextSize;
$blocks['plainTextExtension-'.$plainTextC] = $hex;
break;
}
}
$plainTextC++;
} else if('fe' === $part){ $hex .= $part;
while (!$bytes->isEnd()) { $nextSize = $bytes->bite(1);
if($nextSize !== '00'){
$hex .= $nextSize;
$size = hexdec($nextSize);
$part = $bytes->bite($size);
$hex .= $part;
} else {
$hex .= $nextSize;
$blocks['commentExtension-'.$commentC] = $hex;
break;
}
}
$commentC++;
}
} else if ('2c'===$part){ $hex = $part;
$part = $bytes->bite(2); $hex .= $part;
$part = $bytes->bite(2); $hex .= $part;
$part = $bytes->bite(2); $hex .= $part;
$part = $bytes->bite(2); $hex .= $part;
$part = $bytes->bite(1); $hex .= $part;
$blocks['imageDescriptor-'.$dc] = $hex;
$bin = $this->_fixSize($this->_hexToBin($part), 8);
$localColorTableFlag = bindec(substr($bin, 0, 1));
$sizeOfLocalColorTable = bindec(substr($bin, 5, 3));
if($localColorTableFlag){
$localColorTableLen = 3 * (pow(2, ($sizeOfLocalColorTable + 1)));
$part = $bytes->bite($localColorTableLen);
$blocks['localColorTable-'.$dc] = $part;
}
$part = $bytes->bite(1); $hex = $part;
while ($bytes->isEnd()===false) {
$nextSize = $bytes->bite(1);
$hex .= $nextSize;
if($nextSize !== '00') {
$subBlockLen = hexdec($nextSize);
$subBlock = $bytes->bite($subBlockLen);
$hex .= $subBlock;
} else {
$blocks['imageData-'.$dc] = $hex;
break;
}
}
$dc++;
} else {
$blocks['trailer'] = $part;
break;
}
}
if($blocks['trailer']!=='3b'){
throw new \Exception('Error decoding GIF. Stopped at '.$bytes->getPosition().'. Length is '.$bytes->length().'.');
}
return $blocks;
}
public function expandBlocks($blocks){
$decoded = array();
foreach($blocks as $blockName=>$block){
$bytes = new GifByteStream($block);
if(false !== strpos($blockName, 'header')){
$part = $bytes->bite(3);
$decoded['signature'] = $this->_hexToAscii($part);
$part = $bytes->bite(3);
$decoded['version'] = $this->_hexToAscii($part);
} else if(false !== strpos($blockName, 'logicalScreenDescriptor')){
$part = $bytes->bite(2);
$decoded['canvasWidth'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(2);
$decoded['canvasHeight'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(1);
$bin = $this->_fixSize($this->_hexToBin($part), 8); $decoded['globalColorTableFlag'] = bindec(substr($bin, 0 ,1));
$decoded['colorResolution'] = bindec(substr($bin, 1 ,3));
$decoded['sortFlag'] = bindec(substr($bin, 4 ,1));
$decoded['sizeOfGlobalColorTable'] = bindec(substr($bin, 5 ,3));
$part = $bytes->bite(1);
$decoded['backgroundColorIndex'] = hexdec($part);
$part = $bytes->bite(1);
$decoded['pixelAspectRatio'] = hexdec($part);
} else if(false !== strpos($blockName, 'globalColorTable')){
$decoded['globalColorTable'] = $block;
} else if(false !== strpos($blockName, 'applicationExtension')){
$index = explode('-', $blockName, 2);
$index = $index[1];
$bytes->next(2); $appNameSize = $bytes->bite(1); $appNameSize = hexdec($appNameSize);
$appName = $this->_hexToAscii($bytes->bite($appNameSize));
$subBlocks = array();
while (!$bytes->isEnd()) { $nextSize = $bytes->bite(1);
if($nextSize !== '00'){
$size = hexdec($nextSize);
$subBlocks[] = $bytes->bite($size);
}
}
if($appName==='NETSCAPE2.0'){
$decoded['applicationExtension'][$index]['appId'] = 'NETSCAPE';
$decoded['applicationExtension'][$index]['appCode'] = '2.0';
$decoded['applicationExtension'][$index]['subBlocks'] = $subBlocks;
$decoded['loopCount'] = hexdec($this->_switchEndian(substr($subBlocks[0], 2, 4)));
} else {
$decoded['applicationExtension'][$index]['appId'] = substr($appName, 0, 8);
$decoded['applicationExtension'][$index]['appCode'] = substr($appName, 8, 3);
$decoded['applicationExtension'][$index]['subBlocks'] = $subBlocks;
}
} else if(false !== strpos($blockName, 'graphicControlExtension')) {
$index = explode('-', $blockName, 2);
$index = $index[1];
$bytes->next(3); $part = $bytes->bite(1); $bin = $this->_fixSize($this->_hexToBin($part), 8); $decoded['frames'][$index]['disposalMethod'] = bindec(substr($bin, 3 ,3));
$decoded['frames'][$index]['userInputFlag'] = bindec(substr($bin, 6 ,1));
$decoded['frames'][$index]['transparentColorFlag'] = bindec(substr($bin, 7 ,1));
$part = $bytes->bite(2);
$decoded['frames'][$index]['delayTime'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(1);
$decoded['frames'][$index]['transparentColorIndex'] = hexdec($part);
} else if(false !== strpos($blockName, 'imageDescriptor')) {
$index = explode('-', $blockName, 2);
$index = $index[1];
$bytes->next(1); $part = $bytes->bite(2);
$decoded['frames'][$index]['imageLeft'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(2);
$decoded['frames'][$index]['imageTop'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(2);
$decoded['frames'][$index]['imageWidth'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(2);
$decoded['frames'][$index]['imageHeight'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(1); $bin = $this->_fixSize($this->_hexToBin($part),
8);
$decoded['frames'][$index]['localColorTableFlag'] = bindec(substr($bin, 0, 1));
$decoded['frames'][$index]['interlaceFlag'] = bindec(substr($bin, 1, 1));
$decoded['frames'][$index]['sortFlag'] = bindec(substr($bin, 2, 1));
$decoded['frames'][$index]['sizeOfLocalColorTable'] = bindec(substr($bin, 5, 3));
} else if(false !== strpos($blockName, 'localColorTable')){
$index = explode('-', $blockName, 2);
$index = $index[1];
$decoded['frames'][$index]['localColorTable'] = $block;
} else if(false !== strpos($blockName, 'imageData')) {
$index = explode('-', $blockName, 2);
$index = $index[1];
$decoded['frames'][$index]['imageData'] = $block;
} else if($blockName === 'trailer') {
$decoded['trailer'] = $block;
}
unset($bytes);
}
return $decoded;
}
public function splitFrames($blocks){
$images = array();
if (isset($blocks['frames'])){
foreach($blocks['frames'] as $a=>$unused){
$images[$a] = $blocks;
unset($images[$a]['frames']); foreach($blocks['frames'] as $b=>$frame){
if($a===$b){
$images[$a]['frames'][0] = $frame; break;
}
}
}
}
return $images;
}
public function resize($blocks, $newW, $newH){
$images = $this->splitFrames($blocks);
$firstFrameGd = null;
foreach($images as $imageIndex=>$image){
$hex = $this->encode($image);
$binaryRaw = pack('H*', $hex);
$old = imagecreatefromstring($binaryRaw);
$width = imagesx($old);
$height = imagesy($old);
$new = imagecreatetruecolor($newW, $newH); if($firstFrameGd){
$new = $firstFrameGd;
}
$cX = $newW / $blocks['canvasWidth']; $dX = $image['frames'][0]['imageLeft'];
$cY = $newH / $blocks['canvasHeight'];
$dY = $image['frames'][0]['imageTop'];
imagecopyresampled(
$new,
$old,
$dX * $cX $dY * $cY, 0,
0,
$image['frames'][0]['imageWidth'] * $cX,
$image['frames'][0]['imageHeight'] * $cY,
$width,
$height
);
ob_start();
imagegif($new);
$binaryRaw = ob_get_contents();
ob_end_clean();
if($firstFrameGd===null){
$firstFrameGd = $new;
}
$bytes = $this->load($binaryRaw);
$hexNew = $this->decode($bytes);
$blocks['frames'][$imageIndex]['imageWidth'] = $hexNew['frames'][0]['imageWidth'];
$blocks['frames'][$imageIndex]['imageHeight'] = $hexNew['frames'][0]['imageHeight'];
$blocks['frames'][$imageIndex]['imageLeft'] = $hexNew['frames'][0]['imageLeft'];
$blocks['frames'][$imageIndex]['imageTop'] = $hexNew['frames'][0]['imageTop'];
$blocks['frames'][$imageIndex]['imageData'] = $hexNew['frames'][0]['imageData'];
$blocks['frames'][$imageIndex]['localColorTableFlag'] = $hexNew['globalColorTableFlag'];
$blocks['frames'][$imageIndex]['localColorTable'] = $hexNew['globalColorTable'];
$blocks['frames'][$imageIndex]['sizeOfLocalColorTable'] = $hexNew['sizeOfGlobalColorTable'];
$blocks['frames'][$imageIndex]['transparentColorFlag'] = 0;
}
$blocks['canvasWidth'] = $newW;
$blocks['canvasHeight'] = $newH;
$blocks['globalColorTableFlag'] = 0;
$blocks['globalColorTable'] = '';
return $blocks;
}
private function _asciiToHex($asciiString){
$chars = str_split($asciiString, 1);
$string = '';
foreach($chars as $char){
$string .= dechex(ord($char));
}
return $string;
}
private function _hexToAscii($hexString){
$bytes = str_split($hexString, 2);
$string = '';
foreach($bytes as $byte){
$string .= chr(hexdec($byte)); }
return $string;
}
private function _hexToBin($hexString){
return base_convert($hexString, 16, 2);
}
private function _fixSize($string, $size, $char='0'){
return str_pad($string, $size, $char, STR_PAD_LEFT);
}
private function _switchEndian($hexString) {
return implode('', array_reverse(str_split($hexString, 2)));
}
}