This commit is contained in:
Markus
2022-04-28 09:40:10 +02:00
commit 795794f992
9586 changed files with 1146991 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
<?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\Exceptions;
use CodeIgniter\Exceptions\ExceptionInterface;
use CodeIgniter\Exceptions\FrameworkException;
class ImageException extends FrameworkException implements ExceptionInterface
{
public static function forMissingImage()
{
return new static(lang('Images.sourceImageRequired'));
}
public static function forFileNotSupported()
{
return new static(lang('Images.fileNotSupported'));
}
public static function forMissingAngle()
{
return new static(lang('Images.rotationAngleRequired'));
}
public static function forInvalidDirection(?string $dir = null)
{
return new static(lang('Images.invalidDirection', [$dir]));
}
public static function forInvalidPath()
{
return new static(lang('Images.invalidPath'));
}
public static function forEXIFUnsupported()
{
return new static(lang('Images.exifNotSupported'));
}
public static function forInvalidImageCreate(?string $extra = null)
{
return new static(lang('Images.unsupportedImageCreate') . ' ' . $extra);
}
public static function forSaveFailed()
{
return new static(lang('Images.saveFailed'));
}
public static function forInvalidImageLibraryPath(?string $path = null)
{
return new static(lang('Images.libPathInvalid', [$path]));
}
public static function forImageProcessFailed()
{
return new static(lang('Images.imageProcessFailed'));
}
}

View File

@@ -0,0 +1,775 @@
<?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 CodeIgniter\Images\Image;
use CodeIgniter\Images\ImageHandlerInterface;
use Config\Images;
use InvalidArgumentException;
/**
* Base image handling implementation
*/
abstract class BaseHandler implements ImageHandlerInterface
{
/**
* Configuration settings.
*
* @var Images
*/
protected $config;
/**
* The image/file instance
*
* @var Image
*/
protected $image;
/**
* Whether the image file has been confirmed.
*
* @var bool
*/
protected $verified = false;
/**
* Image width.
*
* @var int
*/
protected $width = 0;
/**
* Image height.
*
* @var int
*/
protected $height = 0;
/**
* File permission mask.
*
* @var int
*/
protected $filePermissions = 0644;
/**
* X-axis.
*
* @var int|null
*/
protected $xAxis = 0;
/**
* Y-axis.
*
* @var int|null
*/
protected $yAxis = 0;
/**
* Master dimensioning.
*
* @var string
*/
protected $masterDim = 'auto';
/**
* Default options for text watermarking.
*
* @var array
*/
protected $textDefaults = [
'fontPath' => null,
'fontSize' => 16,
'color' => 'ffffff',
'opacity' => 1.0,
'vAlign' => 'bottom',
'hAlign' => 'center',
'vOffset' => 0,
'hOffset' => 0,
'padding' => 0,
'withShadow' => false,
'shadowColor' => '000000',
'shadowOffset' => 3,
];
/**
* Image types with support for transparency.
*
* @var array
*/
protected $supportTransparency = [
IMAGETYPE_PNG,
IMAGETYPE_WEBP,
];
/**
* Temporary image used by the different engines.
*
* @var resource|null
*/
protected $resource;
/**
* Constructor.
*
* @param Images|null $config
*/
public function __construct($config = null)
{
$this->config = $config ?? new Images();
}
/**
* Sets another image for this handler to work on.
* Keeps us from needing to continually instantiate the handler.
*
* @return $this
*/
public function withFile(string $path)
{
// Clear out the old resource so that
// it doesn't try to use a previous image
$this->resource = null;
$this->verified = false;
$this->image = new Image($path, true);
$this->image->getProperties(false);
$this->width = $this->image->origWidth;
$this->height = $this->image->origHeight;
return $this;
}
/**
* Make the image resource object if needed
*/
abstract protected function ensureResource();
/**
* Returns the image instance.
*
* @return Image
*/
public function getFile()
{
return $this->image;
}
/**
* Verifies that a file has been supplied and it is an image.
*
* @throws ImageException
*
* @return Image The image instance
*/
protected function image(): Image
{
if ($this->verified) {
return $this->image;
}
// Verify withFile has been called
if (empty($this->image)) {
throw ImageException::forMissingImage();
}
// Verify the loaded image is an Image instance
if (! $this->image instanceof Image) {
throw ImageException::forInvalidPath();
}
// File::__construct has verified the file exists - make sure it is an image
if (! is_int($this->image->imageType)) {
throw ImageException::forFileNotSupported();
}
// Note that the image has been verified
$this->verified = true;
return $this->image;
}
/**
* Returns the temporary image used during the image processing.
* Good for extending the system or doing things this library
* is not intended to do.
*
* @return resource
*/
public function getResource()
{
$this->ensureResource();
return $this->resource;
}
/**
* Load the temporary image used during the image processing.
* Some functions e.g. save() will only copy and not compress
* your image otherwise.
*
* @return $this
*/
public function withResource()
{
$this->ensureResource();
return $this;
}
/**
* Resize the image
*
* @param bool $maintainRatio If true, will get the closest match possible while keeping aspect ratio true.
*
* @return BaseHandler
*/
public function resize(int $width, int $height, bool $maintainRatio = false, string $masterDim = 'auto')
{
// If the target width/height match the source, then we have nothing to do here.
if ($this->image()->origWidth === $width && $this->image()->origHeight === $height) {
return $this;
}
$this->width = $width;
$this->height = $height;
if ($maintainRatio) {
$this->masterDim = $masterDim;
$this->reproportion();
}
return $this->_resize($maintainRatio);
}
/**
* Crops the image to the desired height and width. If one of the height/width values
* is not provided, that value will be set the appropriate value based on offsets and
* image dimensions.
*
* @param int|null $x X-axis coord to start cropping from the left of image
* @param int|null $y Y-axis coord to start cropping from the top of image
*
* @return $this
*/
public function crop(?int $width = null, ?int $height = null, ?int $x = null, ?int $y = null, bool $maintainRatio = false, string $masterDim = 'auto')
{
$this->width = $width;
$this->height = $height;
$this->xAxis = $x;
$this->yAxis = $y;
if ($maintainRatio) {
$this->masterDim = $masterDim;
$this->reproportion();
}
$result = $this->_crop();
$this->xAxis = null;
$this->yAxis = null;
return $result;
}
/**
* Changes the stored image type to indicate the new file format to use when saving.
* Does not touch the actual resource.
*
* @param int $imageType A PHP imageType constant, e.g. https://www.php.net/manual/en/function.image-type-to-mime-type.php
*
* @return $this
*/
public function convert(int $imageType)
{
$this->image()->imageType = $imageType;
return $this;
}
/**
* Rotates the image on the current canvas.
*
* @return $this
*/
public function rotate(float $angle)
{
// Allowed rotation values
$degs = [
90.0,
180.0,
270.0,
];
if (! in_array($angle, $degs, true)) {
throw ImageException::forMissingAngle();
}
// cast angle as an int, for our use
$angle = (int) $angle;
// Reassign the width and height
if ($angle === 90 || $angle === 270) {
$temp = $this->height;
$this->width = $this->height;
$this->height = $temp;
}
// Call the Handler-specific version.
$this->_rotate($angle);
return $this;
}
/**
* Flattens transparencies, default white background
*
* @return $this
*/
public function flatten(int $red = 255, int $green = 255, int $blue = 255)
{
$this->width = $this->image()->origWidth;
$this->height = $this->image()->origHeight;
return $this->_flatten($red, $green, $blue);
}
/**
* Handler-specific method to flattening an image's transparencies.
*
* @return $this
*
* @internal
*/
abstract protected function _flatten(int $red = 255, int $green = 255, int $blue = 255);
/**
* Handler-specific method to handle rotating an image in 90 degree increments.
*
* @return mixed
*/
abstract protected function _rotate(int $angle);
/**
* Flips an image either horizontally or vertically.
*
* @param string $dir Either 'vertical' or 'horizontal'
*
* @return $this
*/
public function flip(string $dir = 'vertical')
{
$dir = strtolower($dir);
if ($dir !== 'vertical' && $dir !== 'horizontal') {
throw ImageException::forInvalidDirection($dir);
}
return $this->_flip($dir);
}
/**
* Handler-specific method to handle flipping an image along its
* horizontal or vertical axis.
*
* @return $this
*/
abstract protected function _flip(string $direction);
/**
* Overlays a string of text over the image.
*
* Valid options:
*
* - color Text Color (hex number)
* - shadowColor Color of the shadow (hex number)
* - hAlign Horizontal alignment: left, center, right
* - vAlign Vertical alignment: top, middle, bottom
* - hOffset
* - vOffset
* - fontPath
* - fontSize
* - shadowOffset
*
* @return $this
*/
public function text(string $text, array $options = [])
{
$options = array_merge($this->textDefaults, $options);
$options['color'] = trim($options['color'], '# ');
$options['shadowColor'] = trim($options['shadowColor'], '# ');
$this->_text($text, $options);
return $this;
}
/**
* Handler-specific method for overlaying text on an image.
*/
abstract protected function _text(string $text, array $options = []);
/**
* Handles the actual resizing of the image.
*
* @return $this
*/
abstract public function _resize(bool $maintainRatio = false);
/**
* Crops the image.
*
* @return $this
*/
abstract public function _crop();
/**
* Return image width.
*
* @return int
*/
abstract public function _getWidth();
/**
* Return the height of an image.
*
* @return int
*/
abstract public function _getHeight();
/**
* Reads the EXIF information from the image and modifies the orientation
* so that displays correctly in the browser. This is especially an issue
* with images taken by smartphones who always store the image up-right,
* but set the orientation flag to display it correctly.
*
* @param bool $silent If true, will ignore exceptions when PHP doesn't support EXIF.
*
* @return $this
*/
public function reorient(bool $silent = false)
{
$orientation = $this->getEXIF('Orientation', $silent);
switch ($orientation) {
case 2:
return $this->flip('horizontal');
case 3:
return $this->rotate(180);
case 4:
return $this->rotate(180)->flip('horizontal');
case 5:
return $this->rotate(270)->flip('horizontal');
case 6:
return $this->rotate(270);
case 7:
return $this->rotate(90)->flip('horizontal');
case 8:
return $this->rotate(90);
default:
return $this;
}
}
/**
* Retrieve the EXIF information from the image, if possible. Returns
* an array of the information, or null if nothing can be found.
*
* EXIF data is only supported fr JPEG & TIFF formats.
*
* @param string|null $key If specified, will only return this piece of EXIF data.
* @param bool $silent If true, will not throw our own exceptions.
*
* @throws ImageException
*
* @return mixed
*/
public function getEXIF(?string $key = null, bool $silent = false)
{
if (! function_exists('exif_read_data')) {
if ($silent) {
return null;
}
throw ImageException::forEXIFUnsupported(); // @codeCoverageIgnore
}
$exif = null; // default
switch ($this->image()->imageType) {
case IMAGETYPE_JPEG:
case IMAGETYPE_TIFF_II:
$exif = @exif_read_data($this->image()->getPathname());
if ($key !== null && is_array($exif)) {
$exif = $exif[$key] ?? false;
}
}
return $exif;
}
/**
* Combine cropping and resizing into a single command.
*
* Supported positions:
* - top-left
* - top
* - top-right
* - left
* - center
* - right
* - bottom-left
* - bottom
* - bottom-right
*
* @param int $height
*
* @return BaseHandler
*/
public function fit(int $width, ?int $height = null, string $position = 'center')
{
$origWidth = $this->image()->origWidth;
$origHeight = $this->image()->origHeight;
[$cropWidth, $cropHeight] = $this->calcAspectRatio($width, $height, $origWidth, $origHeight);
if ($height === null) {
$height = ceil(($width / $cropWidth) * $cropHeight);
}
[$x, $y] = $this->calcCropCoords($cropWidth, $cropHeight, $origWidth, $origHeight, $position);
return $this->crop($cropWidth, $cropHeight, $x, $y)->resize($width, $height);
}
/**
* Calculate image aspect ratio.
*
* @param float|int $width
* @param float|int|null $height
* @param float|int $origWidth
* @param float|int $origHeight
*/
protected function calcAspectRatio($width, $height = null, $origWidth = 0, $origHeight = 0): array
{
if (empty($origWidth) || empty($origHeight)) {
throw new InvalidArgumentException('You must supply the parameters: origWidth, origHeight.');
}
// If $height is null, then we have it easy.
// Calc based on full image size and be done.
if ($height === null) {
$height = ($width / $origWidth) * $origHeight;
return [
$width,
(int) $height,
];
}
$xRatio = $width / $origWidth;
$yRatio = $height / $origHeight;
if ($xRatio > $yRatio) {
return [
$origWidth,
(int) ($origWidth * $height / $width),
];
}
return [
(int) ($origHeight * $width / $height),
$origHeight,
];
}
/**
* Based on the position, will determine the correct x/y coords to
* crop the desired portion from the image.
*
* @param float|int $width
* @param float|int $height
* @param float|int $origWidth
* @param float|int $origHeight
* @param string $position
*/
protected function calcCropCoords($width, $height, $origWidth, $origHeight, $position): array
{
$position = strtolower($position);
$x = $y = 0;
switch ($position) {
case 'top-left':
$x = 0;
$y = 0;
break;
case 'top':
$x = floor(($origWidth - $width) / 2);
$y = 0;
break;
case 'top-right':
$x = $origWidth - $width;
$y = 0;
break;
case 'left':
$x = 0;
$y = floor(($origHeight - $height) / 2);
break;
case 'center':
$x = floor(($origWidth - $width) / 2);
$y = floor(($origHeight - $height) / 2);
break;
case 'right':
$x = ($origWidth - $width);
$y = floor(($origHeight - $height) / 2);
break;
case 'bottom-left':
$x = 0;
$y = $origHeight - $height;
break;
case 'bottom':
$x = floor(($origWidth - $width) / 2);
$y = $origHeight - $height;
break;
case 'bottom-right':
$x = ($origWidth - $width);
$y = $origHeight - $height;
break;
}
return [
$x,
$y,
];
}
/**
* Get the version of the image library in use.
*
* @return string
*/
abstract public function getVersion();
/**
* Saves any changes that have been made to file.
*
* Example:
* $image->resize(100, 200, true)
* ->save($target);
*
* @return bool
*/
abstract public function save(?string $target = null, int $quality = 90);
/**
* Does the driver-specific processing of the image.
*
* @return mixed
*/
abstract protected function process(string $action);
/**
* Provide access to the Image class' methods if they don't exist
* on the handler itself.
*
* @return mixed
*/
public function __call(string $name, array $args = [])
{
if (method_exists($this->image(), $name)) {
return $this->image()->{$name}(...$args);
}
}
/**
* Re-proportion Image Width/Height
*
* When creating thumbs, the desired width/height
* can end up warping the image due to an incorrect
* ratio between the full-sized image and the thumb.
*
* This function lets us re-proportion the width/height
* if users choose to maintain the aspect ratio when resizing.
*/
protected function reproportion()
{
if (($this->width === 0 && $this->height === 0) || $this->image()->origWidth === 0 || $this->image()->origHeight === 0 || (! ctype_digit((string) $this->width) && ! ctype_digit((string) $this->height)) || ! ctype_digit((string) $this->image()->origWidth) || ! ctype_digit((string) $this->image()->origHeight)) {
return;
}
// Sanitize
$this->width = (int) $this->width;
$this->height = (int) $this->height;
if ($this->masterDim !== 'width' && $this->masterDim !== 'height') {
if ($this->width > 0 && $this->height > 0) {
$this->masterDim = ((($this->image()->origHeight / $this->image()->origWidth) - ($this->height / $this->width)) < 0) ? 'width' : 'height';
} else {
$this->masterDim = ($this->height === 0) ? 'width' : 'height';
}
} elseif (($this->masterDim === 'width' && $this->width === 0) || ($this->masterDim === 'height' && $this->height === 0)
) {
return;
}
if ($this->masterDim === 'width') {
$this->height = (int) ceil($this->width * $this->image()->origHeight / $this->image()->origWidth);
} else {
$this->width = (int) ceil($this->image()->origWidth * $this->height / $this->image()->origHeight);
}
}
/**
* Return image width.
*
* accessor for testing; not part of interface
*
* @return int
*/
public function getWidth()
{
return ($this->resource !== null) ? $this->_getWidth() : $this->width;
}
/**
* Return image height.
*
* accessor for testing; not part of interface
*
* @return int
*/
public function getHeight()
{
return ($this->resource !== null) ? $this->_getHeight() : $this->height;
}
}

View File

@@ -0,0 +1,502 @@
<?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();
*/
public function save(?string $target = null, int $quality = 90): bool
{
$original = $target;
$target = empty($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();
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)) {
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
*
* @throws ImageException
*
* @return bool|resource
*/
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'] = $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'] - $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 = ($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);
}
}

View File

@@ -0,0 +1,476 @@
<?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;
use Exception;
use Imagick;
/**
* Class ImageMagickHandler
*
* To make this library as compatible as possible with the broadest
* number of installations, we do not use the Imagick extension,
* but simply use the command line version.
*
* hmm - the width & height accessors at the end use the imagick extension.
*
* FIXME - This needs conversion & unit testing, to use the imagick extension
*/
class ImageMagickHandler extends BaseHandler
{
/**
* Stores image resource in memory.
*
* @var string|null
*/
protected $resource;
/**
* Constructor.
*
* @param Images $config
*
* @throws ImageException
*/
public function __construct($config = null)
{
parent::__construct($config);
// We should never see this, so can't test it
// @codeCoverageIgnoreStart
if (! (extension_loaded('imagick') || class_exists('Imagick'))) {
throw ImageException::forMissingExtension('IMAGICK');
}
// @codeCoverageIgnoreEnd
}
/**
* Handles the actual resizing of the image.
*
* @throws Exception
*
* @return ImageMagickHandler
*/
public function _resize(bool $maintainRatio = false)
{
$source = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
$destination = $this->getResourcePath();
$escape = '\\';
if (PHP_OS_FAMILY === 'Windows') {
$escape = '';
}
$action = $maintainRatio === true
? ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . ' "' . $source . '" "' . $destination . '"'
: ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . "{$escape}! \"" . $source . '" "' . $destination . '"';
$this->process($action);
return $this;
}
/**
* Crops the image.
*
* @throws Exception
*
* @return bool|\CodeIgniter\Images\Handlers\ImageMagickHandler
*/
public function _crop()
{
$source = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
$destination = $this->getResourcePath();
$extent = ' ';
if ($this->xAxis >= $this->width || $this->yAxis > $this->height) {
$extent = ' -background transparent -extent ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . ' ';
}
$action = ' -crop ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . '+' . ($this->xAxis ?? 0) . '+' . ($this->yAxis ?? 0) . $extent . escapeshellarg($source) . ' ' . escapeshellarg($destination);
$this->process($action);
return $this;
}
/**
* Handles the rotation of an image resource.
* Doesn't save the image, but replaces the current resource.
*
* @throws Exception
*
* @return $this
*/
protected function _rotate(int $angle)
{
$angle = '-rotate ' . $angle;
$source = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
$destination = $this->getResourcePath();
$action = ' ' . $angle . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
$this->process($action);
return $this;
}
/**
* Flattens transparencies, default white background
*
* @throws Exception
*
* @return $this
*/
protected function _flatten(int $red = 255, int $green = 255, int $blue = 255)
{
$flatten = "-background 'rgb({$red},{$green},{$blue})' -flatten";
$source = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
$destination = $this->getResourcePath();
$action = ' ' . $flatten . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
$this->process($action);
return $this;
}
/**
* Flips an image along it's vertical or horizontal axis.
*
* @throws Exception
*
* @return $this
*/
protected function _flip(string $direction)
{
$angle = $direction === 'horizontal' ? '-flop' : '-flip';
$source = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
$destination = $this->getResourcePath();
$action = ' ' . $angle . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
$this->process($action);
return $this;
}
/**
* Get driver version
*/
public function getVersion(): string
{
$result = $this->process('-version');
// The first line has the version in it...
preg_match('/(ImageMagick\s[\S]+)/', $result[0], $matches);
return str_replace('ImageMagick ', '', $matches[0]);
}
/**
* Handles all of the grunt work of resizing, etc.
*
* @throws Exception
*
* @return array Lines of output from shell command
*/
protected function process(string $action, int $quality = 100): array
{
// Do we have a vaild library path?
if (empty($this->config->libraryPath)) {
throw ImageException::forInvalidImageLibraryPath($this->config->libraryPath);
}
if ($action !== '-version') {
$this->supportedFormatCheck();
}
if (! preg_match('/convert$/i', $this->config->libraryPath)) {
$this->config->libraryPath = rtrim($this->config->libraryPath, '/') . '/convert';
}
$cmd = $this->config->libraryPath;
$cmd .= $action === '-version' ? ' ' . $action : ' -quality ' . $quality . ' ' . $action;
$retval = 1;
$output = [];
// exec() might be disabled
if (function_usable('exec')) {
@exec($cmd, $output, $retval);
}
// Did it work?
if ($retval > 0) {
throw ImageException::forImageProcessFailed();
}
return $output;
}
/**
* 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();
*/
public function save(?string $target = null, int $quality = 90): bool
{
$original = $target;
$target = empty($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();
// Copy the file through ImageMagick so that it has
// a chance to convert file format.
$action = escapeshellarg($this->resource) . ' ' . escapeshellarg($target);
$this->process($action, $quality);
unlink($this->resource);
return true;
}
/**
* Get Image Resource
*
* This simply creates an image resource handle
* based on the type of image being processed.
* Since ImageMagick is used on the cli, we need to
* ensure we have a temporary file on the server
* that we can use.
*
* To ensure we can use all features, like transparency,
* during the process, we'll use a PNG as the temp file type.
*
* @throws Exception
*
* @return string
*/
protected function getResourcePath()
{
if ($this->resource !== null) {
return $this->resource;
}
$this->resource = WRITEPATH . 'cache/' . time() . '_' . bin2hex(random_bytes(10)) . '.png';
$name = basename($this->resource);
$path = pathinfo($this->resource, PATHINFO_DIRNAME);
$this->image()->copy($path, $name);
return $this->resource;
}
/**
* Make the image resource object if needed
*
* @throws Exception
*/
protected function ensureResource()
{
$this->getResourcePath();
$this->supportedFormatCheck();
}
/**
* Check if given image format is supported
*
* @throws ImageException
*/
protected function supportedFormatCheck()
{
switch ($this->image()->imageType) {
case IMAGETYPE_WEBP:
if (! in_array('WEBP', Imagick::queryFormats(), true)) {
throw ImageException::forInvalidImageCreate(lang('images.webpNotSupported'));
}
break;
}
}
/**
* Handler-specific method for overlaying text on an image.
*
* @throws Exception
*/
protected function _text(string $text, array $options = [])
{
$cmd = '';
// 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;
}
// Font
if (! empty($options['fontPath'])) {
$cmd .= " -font '{$options['fontPath']}'";
}
if (isset($options['hAlign'], $options['vAlign'])) {
switch ($options['hAlign']) {
case 'left':
$xAxis = $options['hOffset'] + $options['padding'];
$yAxis = $options['vOffset'] + $options['padding'];
$gravity = $options['vAlign'] === 'top' ? 'NorthWest' : 'West';
if ($options['vAlign'] === 'bottom') {
$gravity = 'SouthWest';
$yAxis = $options['vOffset'] - $options['padding'];
}
break;
case 'center':
$xAxis = $options['hOffset'] + $options['padding'];
$yAxis = $options['vOffset'] + $options['padding'];
$gravity = $options['vAlign'] === 'top' ? 'North' : 'Center';
if ($options['vAlign'] === 'bottom') {
$yAxis = $options['vOffset'] - $options['padding'];
$gravity = 'South';
}
break;
case 'right':
$xAxis = $options['hOffset'] - $options['padding'];
$yAxis = $options['vOffset'] + $options['padding'];
$gravity = $options['vAlign'] === 'top' ? 'NorthEast' : 'East';
if ($options['vAlign'] === 'bottom') {
$gravity = 'SouthEast';
$yAxis = $options['vOffset'] - $options['padding'];
}
break;
}
$xAxis = $xAxis >= 0 ? '+' . $xAxis : $xAxis;
$yAxis = $yAxis >= 0 ? '+' . $yAxis : $yAxis;
$cmd .= " -gravity {$gravity} -geometry {$xAxis}{$yAxis}";
}
// Color
if (isset($options['color'])) {
[$r, $g, $b] = sscanf("#{$options['color']}", '#%02x%02x%02x');
$cmd .= " -fill 'rgba({$r},{$g},{$b},{$options['opacity']})'";
}
// Font Size - use points....
if (isset($options['fontSize'])) {
$cmd .= " -pointsize {$options['fontSize']}";
}
// Text
$cmd .= " -annotate 0 '{$text}'";
$source = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
$destination = $this->getResourcePath();
$cmd = " '{$source}' {$cmd} '{$destination}'";
$this->process($cmd);
}
/**
* Return the width of an image.
*
* @return int
*/
public function _getWidth()
{
return imagesx(imagecreatefromstring(file_get_contents($this->resource)));
}
/**
* Return the height of an image.
*
* @return int
*/
public function _getHeight()
{
return imagesy(imagecreatefromstring(file_get_contents($this->resource)));
}
/**
* Reads the EXIF information from the image and modifies the orientation
* so that displays correctly in the browser. This is especially an issue
* with images taken by smartphones who always store the image up-right,
* but set the orientation flag to display it correctly.
*
* @param bool $silent If true, will ignore exceptions when PHP doesn't support EXIF.
*
* @return $this
*/
public function reorient(bool $silent = false)
{
$orientation = $this->getEXIF('Orientation', $silent);
switch ($orientation) {
case 2:
return $this->flip('horizontal');
case 3:
return $this->rotate(180);
case 4:
return $this->rotate(180)->flip('horizontal');
case 5:
return $this->rotate(90)->flip('horizontal');
case 6:
return $this->rotate(90);
case 7:
return $this->rotate(270)->flip('horizontal');
case 8:
return $this->rotate(270);
default:
return $this;
}
}
}

133
system/Images/Image.php Normal file
View File

@@ -0,0 +1,133 @@
<?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;
use CodeIgniter\Files\File;
use CodeIgniter\Images\Exceptions\ImageException;
/**
* Encapsulation of an Image file
*/
class Image extends File
{
/**
* The original image width in pixels.
*
* @var float|int
*/
public $origWidth;
/**
* The original image height in pixels.
*
* @var float|int
*/
public $origHeight;
/**
* The image type constant.
*
* @see http://php.net/manual/en/image.constants.php
*
* @var int
*/
public $imageType;
/**
* attributes string with size info:
* 'height="100" width="200"'
*
* @var string
*/
public $sizeStr;
/**
* The image's mime type, i.e. image/jpeg
*
* @var string
*/
public $mime;
/**
* Makes a copy of itself to the new location. If no filename is provided
* it will use the existing filename.
*
* @param string $targetPath The directory to store the file in
* @param string|null $targetName The new name of the copied file.
* @param int $perms File permissions to be applied after copy.
*/
public function copy(string $targetPath, ?string $targetName = null, int $perms = 0644): bool
{
$targetPath = rtrim($targetPath, '/ ') . '/';
$targetName = $targetName ?? $this->getFilename();
if (empty($targetName)) {
throw ImageException::forInvalidFile($targetName);
}
if (! is_dir($targetPath)) {
mkdir($targetPath, 0755, true);
}
if (! copy($this->getPathname(), "{$targetPath}{$targetName}")) {
throw ImageException::forCopyError($targetPath);
}
chmod("{$targetPath}/{$targetName}", $perms);
return true;
}
/**
* Get image properties
*
* A helper function that gets info about the file
*
* @return array|bool
*/
public function getProperties(bool $return = false)
{
$path = $this->getPathname();
if (! $vals = getimagesize($path)) {
throw ImageException::forFileNotSupported();
}
$types = [
IMAGETYPE_GIF => 'gif',
IMAGETYPE_JPEG => 'jpeg',
IMAGETYPE_PNG => 'png',
IMAGETYPE_WEBP => 'webp',
];
$mime = 'image/' . ($types[$vals[2]] ?? 'jpg');
if ($return === true) {
return [
'width' => $vals[0],
'height' => $vals[1],
'image_type' => $vals[2],
'size_str' => $vals[3],
'mime_type' => $mime,
];
}
$this->origWidth = $vals[0];
$this->origHeight = $vals[1];
$this->imageType = $vals[2];
$this->sizeStr = $vals[3];
$this->mime = $mime;
return true;
}
}

View File

@@ -0,0 +1,140 @@
<?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;
/**
* Expected behavior of an Image handler
*/
interface ImageHandlerInterface
{
/**
* Resize the image
*
* @param bool $maintainRatio If true, will get the closest match possible while keeping aspect ratio true.
*
* @return $this
*/
public function resize(int $width, int $height, bool $maintainRatio = false, string $masterDim = 'auto');
/**
* Crops the image to the desired height and width. If one of the height/width values
* is not provided, that value will be set the appropriate value based on offsets and
* image dimensions.
*
* @param int|null $x X-axis coord to start cropping from the left of image
* @param int|null $y Y-axis coord to start cropping from the top of image
*
* @return $this
*/
public function crop(?int $width = null, ?int $height = null, ?int $x = null, ?int $y = null, bool $maintainRatio = false, string $masterDim = 'auto');
/**
* Changes the stored image type to indicate the new file format to use when saving.
* Does not touch the actual resource.
*
* @param int $imageType A PHP imagetype constant, e.g. https://www.php.net/manual/en/function.image-type-to-mime-type.php
*
* @return $this
*/
public function convert(int $imageType);
/**
* Rotates the image on the current canvas.
*
* @return $this
*/
public function rotate(float $angle);
/**
* Flattens transparencies, default white background
*
* @return $this
*/
public function flatten(int $red = 255, int $green = 255, int $blue = 255);
/**
* Reads the EXIF information from the image and modifies the orientation
* so that displays correctly in the browser.
*
* @return ImageHandlerInterface
*/
public function reorient();
/**
* Retrieve the EXIF information from the image, if possible. Returns
* an array of the information, or null if nothing can be found.
*
* @param string|null $key If specified, will only return this piece of EXIF data.
*
* @return mixed
*/
public function getEXIF(?string $key = null);
/**
* Flip an image horizontally or vertically
*
* @param string $dir Direction to flip, either 'vertical' or 'horizontal'
*
* @return $this
*/
public function flip(string $dir = 'vertical');
/**
* Combine cropping and resizing into a single command.
*
* Supported positions:
* - top-left
* - top
* - top-right
* - left
* - center
* - right
* - bottom-left
* - bottom
* - bottom-right
*
* @return $this
*/
public function fit(int $width, int $height, string $position);
/**
* Overlays a string of text over the image.
*
* Valid options:
*
* - color Text Color (hex number)
* - shadowColor Color of the shadow (hex number)
* - hAlign Horizontal alignment: left, center, right
* - vAlign Vertical alignment: top, middle, bottom
* - hOffset
* - vOffset
* - fontPath
* - fontSize
* - shadowOffset
*
* @return $this
*/
public function text(string $text, array $options = []);
/**
* Saves any changes that have been made to file.
*
* Example:
* $image->resize(100, 200, true)
* ->save($target);
*
* @param string $target
*
* @return bool
*/
public function save(?string $target = null, int $quality = 90);
}