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.
 
 
 
 
 
 

511 lines
15 KiB

<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Images\Handlers;
use CodeIgniter\Images\Exceptions\ImageException;
use Config\Images;
/**
* Image handler for GD package
*/
class GDHandler extends BaseHandler
{
/**
* Constructor.
*
* @param Images|null $config
*
* @throws ImageException
*/
public function __construct($config = null)
{
parent::__construct($config);
if (! extension_loaded('gd')) {
throw ImageException::forMissingExtension('GD'); // @codeCoverageIgnore
}
}
/**
* Handles the rotation of an image resource.
* Doesn't save the image, but replaces the current resource.
*/
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
*
* @return $this
*/
protected 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.
*
* @return $this
*/
protected function _flip(string $direction)
{
$srcImg = $this->createImage();
$angle = $direction === 'horizontal' ? IMG_FLIP_HORIZONTAL : IMG_FLIP_VERTICAL;
imageflip($srcImg, $angle);
$this->resource = $srcImg;
return $this;
}
/**
* Get GD version
*
* @return mixed
*/
public function getVersion()
{
if (function_exists('gd_info')) {
$gdVersion = @gd_info();
return preg_replace('/\D/', '', $gdVersion['GD Version']);
}
return false;
}
/**
* Resizes the image.
*
* @return GDHandler
*/
public function _resize(bool $maintainRatio = false)
{
return $this->process('resize');
}
/**
* Crops the image.
*
* @return GDHandler
*/
public function _crop()
{
return $this->process('crop');
}
/**
* Handles all of the grunt work of resizing, etc.
*
* @return $this
*/
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);
// for png and webp we can actually preserve transparency
if (in_array($this->image()->imageType, $this->supportTransparency, true)) {
imagealphablending($dest, false);
imagesavealpha($dest, true);
}
$copy($dest, $src, 0, 0, (int) $this->xAxis, (int) $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 non-empty-string|null $target
*/
public function save(?string $target = null, int $quality = 90): bool
{
$original = $target;
$target = ($target === null || $target === '') ? $this->image()->getPathname() : $target;
// If no new resource has been created, then we're
// simply copy the existing one.
if (empty($this->resource) && $quality === 100) {
if ($original === null) {
return true;
}
$name = basename($target);
$path = pathinfo($target, PATHINFO_DIRNAME);
return $this->image()->copy($path, $name);
}
$this->ensureResource();
// for png and webp we can actually preserve transparency
if (in_array($this->image()->imageType, $this->supportTransparency, true)) {
imagepalettetotruecolor($this->resource);
imagealphablending($this->resource, false);
imagesavealpha($this->resource, true);
}
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;
case IMAGETYPE_WEBP:
if (! function_exists('imagewebp')) {
throw ImageException::forInvalidImageCreate(lang('Images.webpNotSupported'));
}
if (! @imagewebp($this->resource, $target, $quality)) {
throw ImageException::forSaveFailed();
}
break;
default:
throw ImageException::forInvalidImageCreate();
}
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
*
* @return bool|resource
*/
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;
}
return $this->getImageResource($path, $imageType);
}
/**
* Make the image resource object if needed
*/
protected function ensureResource()
{
if ($this->resource === null) {
// if valid image type, make corresponding image resource
$this->resource = $this->getImageResource(
$this->image()->getPathname(),
$this->image()->imageType
);
}
}
/**
* Check if image type is supported and return image resource
*
* @param string $path Image path
* @param int $imageType Image type
*
* @return bool|resource
*
* @throws ImageException
*/
protected function getImageResource(string $path, int $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);
case IMAGETYPE_WEBP:
if (! function_exists('imagecreatefromwebp')) {
throw ImageException::forInvalidImageCreate(lang('Images.webpNotSupported'));
}
return imagecreatefromwebp($path);
default:
throw ImageException::forInvalidImageCreate('Ima');
}
}
/**
* Add text overlay to an image.
*/
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'] *= -1;
}
if ($options['hAlign'] === 'right') {
$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'] - $fontheight - $options['shadowOffset'];
} 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);
}
/**
* Handler-specific method for overlaying text on an image.
*
* @param bool $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 = (int) ($options['opacity'] * 127);
// Allow opacity to be applied to the text
imagealphablending($src, true);
$color = $isShadow ? $options['shadowColor'] : $options['color'];
// shorthand hex, #f00
if (strlen($color) === 3) {
$color = implode('', array_map('str_repeat', str_split($color), [2, 2, 2]));
}
$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, (int) $xAxis, (int) ($yAxis + $options['fontheight']), $color, $options['fontPath'], $text);
} else {
imagestring($src, (int) $options['fontSize'], (int) $xAxis, (int) $yAxis, $text, $color);
}
$this->resource = $src;
}
/**
* Return image width.
*
* @return int
*/
public function _getWidth()
{
return imagesx($this->resource);
}
/**
* Return image height.
*
* @return int
*/
public function _getHeight()
{
return imagesy($this->resource);
}
}