You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

591 lines
14 KiB

<?php
/**
* CodeIgniter
*
* An open source application development framework for PHP
*
* This content is released under the MIT License (MIT)
*
* Copyright (c) 2014-2019 British Columbia Institute of Technology
* Copyright (c) 2019 CodeIgniter Foundation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @package CodeIgniter
* @author CodeIgniter Dev Team
* @copyright 2019 CodeIgniter Foundation
* @license https://opensource.org/licenses/MIT MIT License
* @link https://codeigniter.com
* @since Version 4.0.0
* @filesource
*/
namespace CodeIgniter\Images\Handlers;
use CodeIgniter\Images\Exceptions\ImageException;
/**
* Image handler for GD package
*/
class GDHandler extends BaseHandler
{
/**
* Constructor.
*
* @param type $config
* @throws type
*/
public function __construct($config = null)
{
parent::__construct($config);
// We should never see this, so can't test it
// @codeCoverageIgnoreStart
if (! extension_loaded('gd'))
{
throw ImageException::forMissingExtension('GD');
}
// @codeCoverageIgnoreEnd
}
//--------------------------------------------------------------------
/**
* Handles the rotation of an image resource.
* Doesn't save the image, but replaces the current resource.
*
* @param integer $angle
*
* @return boolean
*/
protected function _rotate(int $angle): bool
{
// Create the image handle
$srcImg = $this->createImage();
// Set the background color
// This won't work with transparent PNG files so we are
// going to have to figure out how to determine the color
// of the alpha channel in a future release.
$white = imagecolorallocate($srcImg, 255, 255, 255);
// Rotate it!
$destImg = imagerotate($srcImg, $angle, $white);
// Kill the file handles
imagedestroy($srcImg);
$this->resource = $destImg;
return true;
}
//--------------------------------------------------------------------
/**
* Flattens transparencies
*
* @param integer $red
* @param integer $green
* @param integer $blue
*
* @return $this
*/
public function _flatten(int $red = 255, int $green = 255, int $blue = 255)
{
$srcImg = $this->createImage();
if (function_exists('imagecreatetruecolor'))
{
$create = 'imagecreatetruecolor';
$copy = 'imagecopyresampled';
}
else
{
$create = 'imagecreate';
$copy = 'imagecopyresized';
}
$dest = $create($this->width, $this->height);
$matte = imagecolorallocate($dest, $red, $green, $blue);
imagefilledrectangle($dest, 0, 0, $this->width, $this->height, $matte);
imagecopy($dest, $srcImg, 0, 0, 0, 0, $this->width, $this->height);
// Kill the file handles
imagedestroy($srcImg);
$this->resource = $dest;
return $this;
}
//--------------------------------------------------------------------
/**
* Flips an image along it's vertical or horizontal axis.
*
* @param string $direction
*
* @return $this
*/
public function _flip(string $direction)
{
$srcImg = $this->createImage();
$width = $this->image->origWidth;
$height = $this->image->origHeight;
if ($direction === 'horizontal')
{
for ($i = 0; $i < $height; $i ++)
{
$left = 0;
$right = $width - 1;
while ($left < $right)
{
$cl = imagecolorat($srcImg, $left, $i);
$cr = imagecolorat($srcImg, $right, $i);
imagesetpixel($srcImg, $left, $i, $cr);
imagesetpixel($srcImg, $right, $i, $cl);
$left ++;
$right --;
}
}
}
else
{
for ($i = 0; $i < $width; $i ++)
{
$top = 0;
$bottom = $height - 1;
while ($top < $bottom)
{
$ct = imagecolorat($srcImg, $i, $top);
$cb = imagecolorat($srcImg, $i, $bottom);
imagesetpixel($srcImg, $i, $top, $cb);
imagesetpixel($srcImg, $i, $bottom, $ct);
$top ++;
$bottom --;
}
}
}
$this->resource = $srcImg;
return $this;
}
//--------------------------------------------------------------------
/**
* Get GD version
*
* @return mixed
*/
public function getVersion()
{
if (function_exists('gd_info'))
{
$gd_version = @gd_info();
return preg_replace('/\D/', '', $gd_version['GD Version']);
}
return false;
}
//--------------------------------------------------------------------
/**
* Resizes the image.
*
* @return boolean|\CodeIgniter\Images\Handlers\GDHandler
*/
public function _resize()
{
return $this->process('resize');
}
//--------------------------------------------------------------------
/**
* Crops the image.
*
* @return boolean|\CodeIgniter\Images\Handlers\GDHandler
*/
public function _crop()
{
return $this->process('crop');
}
//--------------------------------------------------------------------
/**
* Handles all of the grunt work of resizing, etc.
*
* @param string $action
*
* @return $this|bool
*/
protected function process(string $action)
{
$origWidth = $this->image->origWidth;
$origHeight = $this->image->origHeight;
if ($action === 'crop')
{
// Reassign the source width/height if cropping
$origWidth = $this->width;
$origHeight = $this->height;
// Modify the "original" width/height to the new
// values so that methods that come after have the
// correct size to work with.
$this->image->origHeight = $this->height;
$this->image->origWidth = $this->width;
}
// Create the image handle
$src = $this->createImage();
if (function_exists('imagecreatetruecolor'))
{
$create = 'imagecreatetruecolor';
$copy = 'imagecopyresampled';
}
else
{
$create = 'imagecreate';
$copy = 'imagecopyresized';
}
$dest = $create($this->width, $this->height);
if ($this->image->imageType === IMAGETYPE_PNG) // png we can actually preserve transparency
{
imagealphablending($dest, false);
imagesavealpha($dest, true);
}
$copy($dest, $src, 0, 0, $this->xAxis, $this->yAxis, $this->width, $this->height, $origWidth, $origHeight);
imagedestroy($src);
$this->resource = $dest;
return $this;
}
//--------------------------------------------------------------------
/**
* Saves any changes that have been made to file. If no new filename is
* provided, the existing image is overwritten, otherwise a copy of the
* file is made at $target.
*
* Example:
* $image->resize(100, 200, true)
* ->save();
*
* @param string|null $target
* @param integer $quality
*
* @return boolean
*/
public function save(string $target = null, int $quality = 90): bool
{
$target = empty($target) ? $this->image->getPathname() : $target;
switch ($this->image->imageType)
{
case IMAGETYPE_GIF:
if (! function_exists('imagegif'))
{
throw ImageException::forInvalidImageCreate(lang('images.gifNotSupported'));
}
if (! @imagegif($this->resource, $target))
{
throw ImageException::forSaveFailed();
}
break;
case IMAGETYPE_JPEG:
if (! function_exists('imagejpeg'))
{
throw ImageException::forInvalidImageCreate(lang('images.jpgNotSupported'));
}
if (! @imagejpeg($this->resource, $target, $quality))
{
throw ImageException::forSaveFailed();
}
break;
case IMAGETYPE_PNG:
if (! function_exists('imagepng'))
{
throw ImageException::forInvalidImageCreate(lang('images.pngNotSupported'));
}
if (! @imagepng($this->resource, $target))
{
throw ImageException::forSaveFailed();
}
break;
default:
throw ImageException::forInvalidImageCreate();
break;
}
imagedestroy($this->resource);
chmod($target, $this->filePermissions);
return true;
}
//--------------------------------------------------------------------
/**
* Create Image Resource
*
* This simply creates an image resource handle
* based on the type of image being processed
*
* @param string $path
* @param string $imageType
*
* @return resource|boolean
*/
protected function createImage(string $path = '', string $imageType = '')
{
if ($this->resource !== null)
{
return $this->resource;
}
if ($path === '')
{
$path = $this->image->getPathname();
}
if ($imageType === '')
{
$imageType = $this->image->imageType;
}
switch ($imageType)
{
case IMAGETYPE_GIF:
if (! function_exists('imagecreatefromgif'))
{
throw ImageException::forInvalidImageCreate(lang('images.gifNotSupported'));
}
return imagecreatefromgif($path);
case IMAGETYPE_JPEG:
if (! function_exists('imagecreatefromjpeg'))
{
throw ImageException::forInvalidImageCreate(lang('images.jpgNotSupported'));
}
return imagecreatefromjpeg($path);
case IMAGETYPE_PNG:
if (! function_exists('imagecreatefrompng'))
{
throw ImageException::forInvalidImageCreate(lang('images.pngNotSupported'));
}
return imagecreatefrompng($path);
default:
throw ImageException::forInvalidImageCreate('Ima');
}
}
//--------------------------------------------------------------------
/**
* Add text overlay to an image.
*
* @param string $text
* @param array $options
*
* @return void
*/
protected function _text(string $text, array $options = [])
{
// Reverse the vertical offset
// When the image is positioned at the bottom
// we don't want the vertical offset to push it
// further down. We want the reverse, so we'll
// invert the offset. Note: The horizontal
// offset flips itself automatically
if ($options['vAlign'] === 'bottom')
{
$options['vOffset'] = $options['vOffset'] * -1;
}
if ($options['hAlign'] === 'right')
{
$options['hOffset'] = $options['hOffset'] * -1;
}
// Set font width and height
// These are calculated differently depending on
// whether we are using the true type font or not
if (! empty($options['fontPath']))
{
if (function_exists('imagettfbbox'))
{
$temp = imagettfbbox($options['fontSize'], 0, $options['fontPath'], $text);
$temp = $temp[2] - $temp[0];
$fontwidth = $temp / strlen($text);
}
else
{
$fontwidth = $options['fontSize'] - ($options['fontSize'] / 4);
}
$fontheight = $options['fontSize'];
}
else
{
$fontwidth = imagefontwidth($options['fontSize']);
$fontheight = imagefontheight($options['fontSize']);
}
$options['fontheight'] = $fontheight;
$options['fontwidth'] = $fontwidth;
// Set base X and Y axis values
$xAxis = $options['hOffset'] + $options['padding'];
$yAxis = $options['vOffset'] + $options['padding'];
// Set vertical alignment
if ($options['vAlign'] === 'middle')
{
// Don't apply padding when you're in the middle of the image.
$yAxis += ($this->image->origHeight / 2) + ($fontheight / 2) - $options['padding'];
}
elseif ($options['vAlign'] === 'bottom')
{
$yAxis = ($this->image->origHeight - $fontheight - $options['shadowOffset'] - ($fontheight / 2)) - $yAxis;
}
// Set horizontal alignment
if ($options['hAlign'] === 'right')
{
$xAxis += ($this->image->origWidth - ($fontwidth * strlen($text)) - $options['shadowOffset']) - (2 * $options['padding']);
}
elseif ($options['hAlign'] === 'center')
{
$xAxis += floor(($this->image->origWidth - ($fontwidth * strlen($text))) / 2);
}
$options['xAxis'] = $xAxis;
$options['yAxis'] = $yAxis;
if ($options['withShadow'])
{
// Offset from text
$options['xShadow'] = $xAxis + $options['shadowOffset'];
$options['yShadow'] = $yAxis + $options['shadowOffset'];
$this->textOverlay($text, $options, true);
}
$this->textOverlay($text, $options, false);
}
//--------------------------------------------------------------------
/**
* Handler-specific method for overlaying text on an image.
*
* @param string $text
* @param array $options
* @param boolean $isShadow Whether we are drawing the dropshadow or actual text
*/
protected function textOverlay(string $text, array $options = [], bool $isShadow = false)
{
$src = $this->createImage();
/* Set RGB values for shadow
*
* Get the rest of the string and split it into 2-length
* hex values:
*/
$opacity = ($options['opacity'] * 127);
// Allow opacity to be applied to the text
imagealphablending($src, true);
$color = $isShadow ? $options['shadowColor'] : $options['color'];
$color = str_split(substr($color, 0, 6), 2);
$color = imagecolorclosestalpha($src, hexdec($color[0]), hexdec($color[1]), hexdec($color[2]), $opacity);
$xAxis = $isShadow ? $options['xShadow'] : $options['xAxis'];
$yAxis = $isShadow ? $options['yShadow'] : $options['yAxis'];
// Add the shadow to the source image
if (! empty($options['fontPath']))
{
// We have to add fontheight because imagettftext locates the bottom left corner, not top-left corner.
imagettftext($src, $options['fontSize'], 0, $xAxis, $yAxis + $options['fontheight'], $color, $options['fontPath'], $text);
}
else
{
imagestring($src, $options['fontSize'], $xAxis, $yAxis, $text, $color);
}
$this->resource = $src;
}
//--------------------------------------------------------------------
/**
* Return image width.
*
* @return integer
*/
public function _getWidth()
{
return imagesx($this->resource);
}
/**
* Return image height.
*
* @return integer
*/
public function _getHeight()
{
return imagesy($this->resource);
}
}