<?php namespace ProcessWire;

/**
 * ImageSizer Engine Animated GIF by Horst
 * 
 * This module supports resizing and cropping of animated GIFs when using GD-Library
 * (The GD-Library does not support this)
 *
 * This module is based upon the work of
 *
 *    * László Zsidi (initial classes)
 *      http://www.gifs.hu/
 *      http://www.phpclasses.org/gifsplit
 *      http://www.phpclasses.org/gifmerge
 *      (License: Apache 2.0)
 *
 *    * xurei (enhanced classes)
 *      https://github.com/xurei/GIFDecoder_optimized
 *      (License: Apache 2.0)
 *
 * Ported first to ProcessWire module & then to ImageSizerEngine module by Horst Nogajski
 *
 *      https://processwire.com/talk/topic/8386-image-animated-gif/
 */
class ImageSizerEngineAnimatedGif extends ImageSizerEngine {
	
	public static function getModuleInfo() {
		return array(
			'title' => 'Animated GIF Image Sizer',
			'version' => 1,
			'summary' => "Upgrades image manipulations for animated GIFs.",
			'author' => 'Horst Nogajski',
		);
	}

	/**
	 * Class constructor
	 * 
	 */
	public function __construct() {
		parent::__construct();
		$this->set('enginePriority', 9); // use a late priority so that optional other installed engines get the job (first) if they support animated gifs
	}

	/**
	 * Get valid image source formats
	 * 
	 * @return array
	 * 
	 */
	protected function validSourceImageFormats() {
		return array('GIF');
	}

	/**
	 * Get valid target image formats
	 * 
	 * @return array
	 * 
	 */
	protected function validTargetImageFormats() {
		return $this->validSourceImageFormats();
	}
	
	/**
	 * Get library version string
	 *
	 * @return string Returns version string or blank string if not applicable/available
	 * @since 3.0.138
	 *
	 */
	public function getLibraryVersion() {
		$gd = gd_info();
		return isset($gd['GD Version']) ? $gd['GD Version'] : '';
	}

	/**
	 * Is GD supported?
	 *
	 * @param string $action
	 * @return bool
	 *
	 */
	public function supported($action = 'imageformat') {
		// first we check parts that are mandatory for all $actions
		if(!function_exists('gd_info')) return false;

		// and if it passes the mandatory requirements, we check particularly aspects here
		switch($action) {
			case 'imageformat':
				// compare current imagefile infos fetched from ImageInspector
				$requested = $this->getImageInfo(false);
				switch($requested) {
					case 'gif-anim':
					case 'gif-trans-anim':
						return true;
					default:
						return false;
				}
				break;

			case 'install':
				return true;

			default:
				return false;
		}
	}

	/**
	 * Process the image resize
	 *
	 * Processing is as follows:
	 *    1. first do a check if the given image(type) can be processed, if not do an early return false
	 *    2. than (try) to process all required steps, if one failes, return false
	 *    3. if all is successful, finally return true
	 *
	 * @param string $srcFilename Source file
	 * @param string $dstFilename Destination file
	 * @param int $fullWidth Current width
	 * @param int $fullHeight Current height
	 * @param int $finalWidth Requested final width
	 * @param int $finalHeight Requested final height
	 * @return bool True if successful, false if not
	 * @throws WireException
	 *
	 */
	protected function processResize($srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight) {
		$this->modified = false;
		if(isset($this->info['bits'])) $this->imageDepth = $this->info['bits'];
		$this->imageFormat = strtoupper(str_replace('image/', '', $this->info['mime']));

		if(!in_array($this->imageFormat, $this->validSourceImageFormats())) {
			throw new WireException(sprintf($this->_("loaded file '%s' is not in the list of valid images"), basename($dstFilename)));
		}

		require_once(__DIR__ . '/gif_encoder.php');
		require_once(__DIR__ . '/gif_decoder.php');
		
		$this->setTimeLimit(120);

		$zoom = $this->getFocusZoomPercent();
		if($zoom > 1) {
			// we need to configure a cropExtra call to respect the zoom factor
			$this->cropExtra = $this->getFocusZoomCropDimensions($zoom, $fullWidth, $fullHeight, $finalWidth, $finalHeight);
			$this->cropping = false;
		}

		// if extra crop manipulation is requested, it is processed first
        if(is_array($this->cropExtra) && 4 == count($this->cropExtra)) { // crop before resize
            list($cropX, $cropY, $cropWidth, $cropHeight) = $this->cropExtra;
            $bg = null;
            $gif = new ISEAG_GIFDecoder(file_get_contents($srcFilename));
            $originalFramesMeta = $gif->GIFGetFramesMeta();
            if(count($originalFramesMeta) <= 0) return false;
            $this->meta = array(
                'delays'    => $gif->GIFGetDelays(),
                'loops'     => $gif->GIFGetLoop(),
                'disposal'  => $gif->GIFGetDisposal(),
                'tr'        => $gif->GIFGetTransparentR(),
                'tg'        => $gif->GIFGetTransparentG(),
                'tb'        => $gif->GIFGetTransparentB(),
                'trans'     => (0 == $gif->GIFGetTransparentI() ? false : true)
            );
            $originalFrames = $gif->GIFGetFrames();
            $newFrames = array();
            foreach($originalFrames as $k => $v) {
                $frame = @imagecreatefromstring($v);
                if(!is_resource($frame)) continue;
                if(!is_resource($bg)) {
                    $bg = imagecreatetruecolor($fullWidth, $fullHeight);
                    $this->prepareGDimage($bg);
                }
                $srcX = 0;
                $srcY = 0;
                $srcW = imagesx($frame);
                $srcH = imagesy($frame);
                $dstX = $originalFramesMeta[$k]['left'];
                $dstY = $originalFramesMeta[$k]['top'];
                $dstW = $originalFramesMeta[$k]['width'];
                $dstH = $originalFramesMeta[$k]['height'];
                imagecopy($bg, $frame, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH);
                $newimg = imagecreatetruecolor($cropWidth, $cropHeight);
                $this->prepareGDimage($newimg);
                imagecopy($newimg, $bg, 0, 0, $cropX, $cropY, $cropWidth, $cropHeight);
                array_push($newFrames, $newimg);
                $originalFrames[$k] = null;
            }
            if(count($newFrames) > 0) {
                $frames = array();
                foreach($newFrames as $nf) {
                    if(!is_resource($nf)) continue;
                    ob_start();
                    imagegif($nf);
                    $gifdata = ob_get_clean();
                    array_push($frames, $gifdata);
                    @imagedestroy($nf);
                }
                $gifmerge = new ISEAG_GIFEncoder(
                    $frames,
                    $this->meta['delays'],
                    $this->meta['loops'],
                    $this->meta['disposal'],
                    $this->meta['tr'], $this->meta['tg'], $this->meta['tb'],
                    'bin'
                );
                $result = false === fwrite(fopen($srcFilename, 'wb'), $gifmerge->GetAnimation()) ? false : true;
                if($result) {
                    $fullWidth = $cropWidth;
                    $fullHeight = $cropHeight;
                    $this->image = array('width' => $fullWidth, 'height' => $fullHeight);
                }
            } else {
                // $result = false;
            }
            if(isset($bg) && is_resource($bg)) @imagedestroy($bg);
            if(isset($frame) && is_resource($frame)) @imagedestroy($frame);
            if(isset($newimg) && is_resource($newimg)) @imagedestroy($newimg);
            unset($gif, $gifmerge, $originalFrames, $originalFramesMeta, $newFrames, $cropHeight, $cropWidth, $cropX, $cropY, $dstH, $dstW, $dstX, $dstY, $frames, $nf, $srcH, $srcW, $srcX, $srcY);
            $this->meta = null;
        }

		// regular resize / crop manipulation starts here
        $bgX = $bgY = 0;
        $bgWidth = $fullWidth;
        $bgHeight = $fullHeight;
        $resizemethod = $this->getResizeMethod($bgWidth, $bgHeight, $finalWidth, $finalHeight, $bgX, $bgY);
        if(0 == $resizemethod) return true; // if same size or disallowed greater size is requested, we stop here and leave the original copy as is

        $gif = new ISEAG_GIFdecoder(file_get_contents($srcFilename));
        $originalFramesMeta = $gif->GIFGetFramesMeta();
        if(count($originalFramesMeta) <= 0) return false;
        $this->meta = array(
            'delays'    => $gif->GIFGetDelays(),
            'loops'     => $gif->GIFGetLoop(),
            'disposal'  => $gif->GIFGetDisposal(),
            'tr'        => $gif->GIFGetTransparentR(),
            'tg'        => $gif->GIFGetTransparentG(),
            'tb'        => $gif->GIFGetTransparentB(),
            'trans'     => (0 == $gif->GIFGetTransparentI() ? false : true)
        );
        $originalFrames = $gif->GIFGetFrames();
        $newFrames = array();

        if(2 == $resizemethod) { // 2 = resize with aspect ratio
            $bg = null;
            // $ratio = 1.0;
            $ratio_w = $fullWidth / $finalWidth;
            $ratio_h = $fullHeight / $finalHeight;
            $ratio = ($ratio_h > $ratio_w ? $ratio_h : $ratio_w);
            foreach($originalFrames as $k => $v) {
                $frame = @imagecreatefromstring($v);
                if(!is_resource($frame)) continue;
                $newimg = imagecreatetruecolor($finalWidth, $finalHeight);
                $this->prepareGDimage($newimg);
                if(is_resource($bg)) {
                    imagecopy($newimg, $bg, 0, 0, 0, 0, $finalWidth, $finalHeight);
                }
                $srcX = 0;
                $srcY = 0;
                $srcW = imagesx($frame);
                $srcH = imagesy($frame);
                $dstX = floor($originalFramesMeta[$k]['left'] / $ratio);
                $dstY = floor($originalFramesMeta[$k]['top'] / $ratio);
                $dstW = ceil($originalFramesMeta[$k]['width'] / $ratio);
                $dstH = ceil($originalFramesMeta[$k]['height'] / $ratio);
                imagecopyresampled($newimg, $frame, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH);
                array_push($newFrames, $newimg);
                if(!is_resource($bg)) {
                    $bg = imagecreatetruecolor($finalWidth, $finalHeight);
                    $this->prepareGDimage($bg);
                }
                imagecopy($bg, $newimg, 0, 0, 0, 0, $finalWidth, $finalHeight);
                $originalFrames[$k] = null;
            }
        }

        if(4 == $resizemethod) { // 4 = resize and crop from center with aspect ratio
            $bg = null;
            // $ratio = 1.0;
            $ratio_w = $fullWidth / $bgWidth;
            $ratio_h = $fullHeight / $bgHeight;
            $ratio = ($ratio_h > $ratio_w ? $ratio_h : $ratio_w);
            foreach($originalFrames as $k => $v) {
                $frame = @imagecreatefromstring($v);
                if(!is_resource($frame)) continue;
                $newimg = imagecreatetruecolor($bgWidth, $bgHeight);
                $this->prepareGDimage($newimg);
                if(is_resource($bg)) {
                    imagecopy($newimg, $bg, 0, 0, 0, 0, $bgWidth, $bgHeight);
                }
                $srcX = 0;
                $srcY = 0;
                $srcW = imagesx($frame);
                $srcH = imagesy($frame);
                $dstX = floor($originalFramesMeta[$k]['left'] / $ratio);
                $dstY = floor($originalFramesMeta[$k]['top'] / $ratio);
                $dstW = ceil($originalFramesMeta[$k]['width'] / $ratio);
                $dstH = ceil($originalFramesMeta[$k]['height'] / $ratio);
                imagecopyresampled($newimg, $frame, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH);
                if(!is_resource($bg)) {
                    $bg = imagecreatetruecolor($bgWidth, $bgHeight);
                    $this->prepareGDimage($bg);
                }
                imagecopy($bg, $newimg, 0, 0, 0, 0, $bgWidth, $bgHeight);
                $newimg = imagecreatetruecolor($finalWidth, $finalHeight);
                $this->prepareGDimage($newimg);
                imagecopy($newimg, $bg, 0, 0, $bgX, $bgY, $finalWidth, $finalHeight);
                array_push($newFrames, $newimg);
                $originalFrames[$k] = null;
            }
        }

        if(count($newFrames) > 0) {
            $frames = array();
            foreach($newFrames as $nf) {
                if(!is_resource($nf)) continue;
                ob_start();
                imagegif($nf);
                $gifdata = ob_get_clean();
                array_push($frames, $gifdata);
                @imagedestroy($nf);
            }
            $gifmerge = new ISEAG_GIFEncoder(
                $frames,
                $this->meta['delays'],
                $this->meta['loops'],
                $this->meta['disposal'],
                $this->meta['tr'], $this->meta['tg'], $this->meta['tb'],
                'bin'
            );
            $result = false === fwrite(fopen($dstFilename, 'wb'), $gifmerge->GetAnimation()) ? false : true;
        } else {
            $result = false;
        }

        if(isset($bg) && is_resource($bg)) @imagedestroy($bg);
        if(isset($frame) && is_resource($frame)) @imagedestroy($frame);
        if(isset($newimg) && is_resource($newimg)) @imagedestroy($newimg);

		if(!$result) {
			return false;
		}

		$this->modified = true;
		return true;
	}

	/**
	 * Helper for transparent background preparation
	 *
	 * @param resource $gdimage by reference $gdimage
	 * @return void
	 *
	 */
    protected function prepareGDimage(&$gdimage) {
        if(!$this->meta['trans']) return;
        $transparentNew = imagecolorallocate($gdimage, $this->meta['tr'], $this->meta['tg'], $this->meta['tb']);
        $transparentNewIndex = imagecolortransparent($gdimage, $transparentNew);
        imagefill($gdimage, 0, 0, $transparentNewIndex);
    }


	/**
	 * Module install
	 * 
	 * @throws WireException
	 * 
	 */
	public function ___install() {
		if(!$this->supported('install')) {
			throw new WireException("This module requires that you have PHP's GD image library bundled or installed");
		}
	}
	
}
