377 lines
14 KiB
Text
377 lines
14 KiB
Text
|
<?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");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|