diff --git a/app/Config/Images.php b/app/Config/Images.php index a33ddadb9a5c..68e415e049e0 100644 --- a/app/Config/Images.php +++ b/app/Config/Images.php @@ -16,6 +16,8 @@ class Images extends BaseConfig /** * The path to the image library. * Required for ImageMagick, GraphicsMagick, or NetPBM. + * + * @deprecated 4.7.0 No longer used. */ public string $libraryPath = '/usr/local/bin/convert'; diff --git a/system/Images/Exceptions/ImageException.php b/system/Images/Exceptions/ImageException.php index 91bf416d20ac..46651fbf706d 100644 --- a/system/Images/Exceptions/ImageException.php +++ b/system/Images/Exceptions/ImageException.php @@ -100,6 +100,8 @@ public static function forSaveFailed() /** * Thrown when the image library path is invalid. * + * @deprecated 4.7.0 No longer used. + * * @return static */ public static function forInvalidImageLibraryPath(?string $path = null) diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index f1448efa7f8a..08886937a4b1 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -13,27 +13,30 @@ namespace CodeIgniter\Images\Handlers; -use CodeIgniter\I18n\Time; use CodeIgniter\Images\Exceptions\ImageException; use Config\Images; -use Exception; use Imagick; +use ImagickDraw; +use ImagickDrawException; +use ImagickException; +use ImagickPixel; +use ImagickPixelException; /** - * Class ImageMagickHandler - * - * FIXME - This needs conversion & unit testing, to use the imagick extension + * Image handler for Imagick extension. */ class ImageMagickHandler extends BaseHandler { /** - * Stores image resource in memory. + * Stores Imagick instance. * - * @var string|null + * @var Imagick|null */ protected $resource; /** + * Constructor. + * * @param Images $config * * @throws ImageException @@ -42,25 +45,95 @@ public function __construct($config = null) { parent::__construct($config); - if (! extension_loaded('imagick') && ! class_exists(Imagick::class)) { - throw ImageException::forMissingExtension('IMAGICK'); // @codeCoverageIgnore + if (! extension_loaded('imagick')) { + throw ImageException::forMissingExtension('IMAGICK'); // @codeCoverageIgnore } + } - $cmd = $this->config->libraryPath; + /** + * Loads the image for manipulation. + * + * @return void + * + * @throws ImageException + */ + protected function ensureResource() + { + if (! $this->resource instanceof Imagick) { + // Verify that we have a valid image + $this->image(); - if ($cmd === '') { - throw ImageException::forInvalidImageLibraryPath($cmd); - } + try { + $this->resource = new Imagick(); + $this->resource->readImage($this->image()->getPathname()); - if (preg_match('/convert$/i', $cmd) !== 1) { - $cmd = rtrim($cmd, '\/') . '/convert'; + // Check for valid image + if ($this->resource->getImageWidth() === 0 || $this->resource->getImageHeight() === 0) { + throw ImageException::forInvalidImageCreate($this->image()->getPathname()); + } - $this->config->libraryPath = $cmd; + $this->supportedFormatCheck(); + } catch (ImagickException $e) { + throw ImageException::forInvalidImageCreate($e->getMessage()); + } } + } + + /** + * Handles all the grunt work of resizing, etc. + * + * @param string $action Type of action to perform + * @param int $quality Quality setting for Imagick operations + * + * @return $this + * + * @throws ImageException + */ + protected function process(string $action, int $quality = 100) + { + $this->image(); + + $this->ensureResource(); + + try { + switch ($action) { + case 'resize': + $this->resource->resizeImage( + $this->width, + $this->height, + Imagick::FILTER_LANCZOS, + 0, + ); + break; - if (! is_file($cmd)) { - throw ImageException::forInvalidImageLibraryPath($cmd); + case 'crop': + $width = $this->width; + $height = $this->height; + $xAxis = $this->xAxis ?? 0; + $yAxis = $this->yAxis ?? 0; + + $this->resource->cropImage( + $width, + $height, + $xAxis, + $yAxis, + ); + + // Reset canvas to cropped size + $this->resource->setImagePage(0, 0, 0, 0); + break; + } + + // Handle transparency for supported image types + if (in_array($this->image()->imageType, $this->supportTransparency, true) + && $this->resource->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED) { + $this->resource->setImageAlphaChannel(Imagick::ALPHACHANNEL_OPAQUE); + } + } catch (ImagickException) { + throw ImageException::forImageProcessFailed(); } + + return $this; } /** @@ -68,50 +141,56 @@ public function __construct($config = null) * * @return ImageMagickHandler * - * @throws Exception + * @throws ImagickException */ public function _resize(bool $maintainRatio = false) { - $source = ! empty($this->resource) ? $this->resource : $this->image()->getPathname(); - $destination = $this->getResourcePath(); + if ($maintainRatio) { + // If maintaining a ratio, we need a custom approach + $this->ensureResource(); - $escape = '\\'; + // Use thumbnailImage which preserves an aspect ratio + $this->resource->thumbnailImage($this->width, $this->height, true); - if (PHP_OS_FAMILY === 'Windows') { - $escape = ''; + return $this; } - $action = $maintainRatio - ? ' -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; + // Use the common process() method for normal resizing + return $this->process('resize'); } /** * Crops the image. * - * @return bool|ImageMagickHandler + * @return $this * - * @throws Exception + * @throws ImagickException */ public function _crop() { - $source = ! empty($this->resource) ? $this->resource : $this->image()->getPathname(); - $destination = $this->getResourcePath(); + // Use the common process() method for cropping + $result = $this->process('crop'); - $extent = ' '; - if ($this->xAxis >= $this->width || $this->yAxis > $this->height) { - $extent = ' -background transparent -extent ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . ' '; - } + // Handle a case where crop dimensions exceed the original image size + if ($this->resource instanceof Imagick) { + $imgWidth = $this->resource->getImageWidth(); + $imgHeight = $this->resource->getImageHeight(); - $action = ' -crop ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . '+' . ($this->xAxis ?? 0) . '+' . ($this->yAxis ?? 0) . $extent . escapeshellarg($source) . ' ' . escapeshellarg($destination); + if ($this->xAxis >= $imgWidth || $this->yAxis >= $imgHeight) { + // Create transparent background + $background = new Imagick(); + $background->newImage($this->width, $this->height, new ImagickPixel('transparent')); + $background->setImageFormat($this->resource->getImageFormat()); - $this->process($action); + // Composite our image on the background + $background->compositeImage($this->resource, Imagick::COMPOSITE_OVER, 0, 0); - return $this; + // Replace our resource + $this->resource = $background; + } + } + + return $result; } /** @@ -120,18 +199,18 @@ public function _crop() * * @return $this * - * @throws Exception + * @throws ImagickException */ protected function _rotate(int $angle) { - $angle = '-rotate ' . $angle; - - $source = ! empty($this->resource) ? $this->resource : $this->image()->getPathname(); - $destination = $this->getResourcePath(); + $this->ensureResource(); - $action = ' ' . $angle . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination); + // Create transparent background + $this->resource->setImageBackgroundColor(new ImagickPixel('transparent')); + $this->resource->rotateImage(new ImagickPixel('transparent'), $angle); - $this->process($action); + // Reset canvas dimensions + $this->resource->setImagePage($this->resource->getImageWidth(), $this->resource->getImageHeight(), 0, 0); return $this; } @@ -141,88 +220,100 @@ protected function _rotate(int $angle) * * @return $this * - * @throws Exception + * @throws ImagickException|ImagickPixelException */ 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->ensureResource(); - $this->process($action); + // Create background + $bg = new ImagickPixel("rgb({$red},{$green},{$blue})"); + + // Create a new canvas with the background color + $canvas = new Imagick(); + $canvas->newImage( + $this->resource->getImageWidth(), + $this->resource->getImageHeight(), + $bg, + $this->resource->getImageFormat(), + ); + + // Composite our image on the background + $canvas->compositeImage( + $this->resource, + Imagick::COMPOSITE_OVER, + 0, + 0, + ); + + // Replace our resource with the flattened version + $this->resource->clear(); + $this->resource = $canvas; return $this; } /** - * Flips an image along it's vertical or horizontal axis. + * Flips an image along its vertical or horizontal axis. * * @return $this * - * @throws Exception + * @throws ImagickException */ 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->ensureResource(); - $this->process($action); + if ($direction === 'horizontal') { + $this->resource->flopImage(); + } else { + $this->resource->flipImage(); + } return $this; } /** - * Get driver version + * Get a driver version + * + * @return string */ - public function getVersion(): string + public function getVersion() { - $versionString = $this->process('-version')[0]; - preg_match('/ImageMagick\s(?P[\S]+)/', $versionString, $matches); + $version = Imagick::getVersion(); + + if (preg_match('/ImageMagick\s+(\d+\.\d+\.\d+)/', $version['versionString'], $matches)) { + return $matches[1]; + } - return $matches['version']; + return ''; } /** - * Handles all of the grunt work of resizing, etc. + * Check if a given image format is supported * - * @return array Lines of output from shell command + * @return void * - * @throws Exception + * @throws ImageException */ - protected function process(string $action, int $quality = 100): array + protected function supportedFormatCheck() { - if ($action !== '-version') { - $this->supportedFormatCheck(); - } - - $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); + if (! $this->resource instanceof Imagick) { + return; } - // Did it work? - if ($retval > 0) { - throw ImageException::forImageProcessFailed(); + switch ($this->image()->imageType) { + case IMAGETYPE_WEBP: + if (! in_array('WEBP', Imagick::queryFormats(), true)) { + throw ImageException::forInvalidImageCreate(lang('images.webpNotSupported')); + } + break; } - - 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 + * Saves any changes that have been made to the file. If no new filename is + * provided, the existing image is overwritten; otherwise a copy of the * file is made at $target. * * Example: @@ -230,6 +321,8 @@ protected function process(string $action, int $quality = 100): array * ->save(); * * @param non-empty-string|null $target + * + * @throws ImagickException */ public function save(?string $target = null, int $quality = 90): bool { @@ -238,7 +331,7 @@ public function save(?string $target = null, int $quality = 90): bool // If no new resource has been created, then we're // simply copy the existing one. - if (empty($this->resource) && $quality === 100) { + if (! $this->resource instanceof Imagick && $quality === 100) { if ($original === null) { return true; } @@ -251,192 +344,172 @@ public function save(?string $target = null, int $quality = 90): bool $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; - } + $this->resource->setImageCompressionQuality($quality); - /** - * 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. - * - * @return string - * - * @throws Exception - */ - protected function getResourcePath() - { - if ($this->resource !== null) { - return $this->resource; + if ($target !== null) { + $extension = pathinfo($target, PATHINFO_EXTENSION); + $this->resource->setImageFormat($extension); } - $this->resource = WRITEPATH . 'cache/' . Time::now()->getTimestamp() . '_' . bin2hex(random_bytes(10)) . '.png'; + try { + $result = $this->resource->writeImage($target); - $name = basename($this->resource); - $path = pathinfo($this->resource, PATHINFO_DIRNAME); + chmod($target, $this->filePermissions); - $this->image()->copy($path, $name); + $this->resource->clear(); + $this->resource = null; - return $this->resource; - } - - /** - * Make the image resource object if needed - * - * @return void - * - * @throws Exception - */ - protected function ensureResource() - { - $this->getResourcePath(); - - $this->supportedFormatCheck(); - } - - /** - * Check if given image format is supported - * - * @return void - * - * @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; + return $result; + } catch (ImagickException) { + throw ImageException::forSaveFailed(); } } /** * Handler-specific method for overlaying text on an image. * - * @throws Exception + * @throws ImagickDrawException|ImagickException|ImagickPixelException */ protected function _text(string $text, array $options = []) { - $xAxis = 0; - $yAxis = 0; - $gravity = ''; - $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'] *= -1; + $this->ensureResource(); + + $draw = new ImagickDraw(); + + if (isset($options['fontPath'])) { + $draw->setFont($options['fontPath']); } - if ($options['hAlign'] === 'right') { - $options['hOffset'] *= -1; + if (isset($options['fontSize'])) { + $draw->setFontSize($options['fontSize']); } - // Font - if (! empty($options['fontPath'])) { - $cmd .= " -font '{$options['fontPath']}'"; + if (isset($options['color'])) { + $color = $options['color']; + + // Shorthand hex, #f00 + if (strlen($color) === 3) { + $color = implode('', array_map(str_repeat(...), str_split($color), [2, 2, 2])); + } + + [$r, $g, $b] = sscanf("#{$color}", '#%02x%02x%02x'); + $opacity = $options['opacity'] ?? 1.0; + $draw->setFillColor(new ImagickPixel("rgba({$r},{$g},{$b},{$opacity})")); } - if (isset($options['hAlign'], $options['vAlign'])) { + // Calculate text positioning + $imgWidth = $this->resource->getImageWidth(); + $imgHeight = $this->resource->getImageHeight(); + $xAxis = 0; + $yAxis = 0; + + // Default padding + $padding = $options['padding'] ?? 0; + + if (isset($options['hAlign'])) { + $hOffset = $options['hOffset'] ?? 0; + 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']; - } + $xAxis = $hOffset + $padding; + $draw->setTextAlignment(Imagick::ALIGN_LEFT); 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'; - } + $xAxis = $imgWidth / 2 + $hOffset; + $draw->setTextAlignment(Imagick::ALIGN_CENTER); 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']; - } + $xAxis = $imgWidth - $hOffset - $padding; + $draw->setTextAlignment(Imagick::ALIGN_RIGHT); break; } + } - $xAxis = $xAxis >= 0 ? '+' . $xAxis : $xAxis; - $yAxis = $yAxis >= 0 ? '+' . $yAxis : $yAxis; + if (isset($options['vAlign'])) { + $vOffset = $options['vOffset'] ?? 0; - $cmd .= " -gravity {$gravity} -geometry {$xAxis}{$yAxis}"; - } + switch ($options['vAlign']) { + case 'top': + $yAxis = $vOffset + $padding + ($options['fontSize'] ?? 16); + break; - // Color - if (isset($options['color'])) { - [$r, $g, $b] = sscanf("#{$options['color']}", '#%02x%02x%02x'); + case 'middle': + $yAxis = $imgHeight / 2 + $vOffset; + break; - $cmd .= " -fill 'rgba({$r},{$g},{$b},{$options['opacity']})'"; + case 'bottom': + // Note: Vertical offset is inverted for bottom alignment as per original implementation + $yAxis = $vOffset < 0 ? $imgHeight + $vOffset - $padding : $imgHeight - $vOffset - $padding; + break; + } } - // Font Size - use points.... - if (isset($options['fontSize'])) { - $cmd .= " -pointsize {$options['fontSize']}"; - } + if (isset($options['withShadow'])) { + $shadow = clone $draw; + + if (isset($options['shadowColor'])) { + $shadowColor = $options['shadowColor']; - // Text - $cmd .= " -annotate 0 '{$text}'"; + // Shorthand hex, #f00 + if (strlen($shadowColor) === 3) { + $shadowColor = implode('', array_map(str_repeat(...), str_split($shadowColor), [2, 2, 2])); + } - $source = ! empty($this->resource) ? $this->resource : $this->image()->getPathname(); - $destination = $this->getResourcePath(); + [$sr, $sg, $sb] = sscanf("#{$shadowColor}", '#%02x%02x%02x'); + $shadow->setFillColor(new ImagickPixel("rgb({$sr},{$sg},{$sb})")); + } else { + $shadow->setFillColor(new ImagickPixel('rgba(0,0,0,0.5)')); + } - $cmd = " '{$source}' {$cmd} '{$destination}'"; + $offset = $options['shadowOffset'] ?? 3; - $this->process($cmd); + $this->resource->annotateImage( + $shadow, + $xAxis + $offset, + $yAxis + $offset, + 0, + $text, + ); + } + + // Draw the main text + $this->resource->annotateImage( + $draw, + $xAxis, + $yAxis, + 0, + $text, + ); } /** * Return the width of an image. * * @return int + * + * @throws ImagickException */ public function _getWidth() { - return imagesx(imagecreatefromstring(file_get_contents($this->resource))); + $this->ensureResource(); + + return $this->resource->getImageWidth(); } /** * Return the height of an image. * * @return int + * + * @throws ImagickException */ public function _getHeight() { - return imagesy(imagecreatefromstring(file_get_contents($this->resource))); + $this->ensureResource(); + + return $this->resource->getImageHeight(); } /** diff --git a/system/Language/en/Images.php b/system/Language/en/Images.php index 41dbe973aaa5..cdfe9ccbb181 100644 --- a/system/Language/en/Images.php +++ b/system/Language/en/Images.php @@ -24,7 +24,6 @@ 'unsupportedImageCreate' => 'Your server does not support the GD function required to process this type of image.', 'jpgOrPngRequired' => 'The image resize protocol specified in your preferences only works with JPEG or PNG image types.', 'rotateUnsupported' => 'Image rotation does not appear to be supported by your server.', - 'libPathInvalid' => 'The path to your image library is not correct. Please set the correct path in your image preferences. "{0}"', 'imageProcessFailed' => 'Image processing failed. Please verify that your server supports the chosen protocol and that the path to your image library is correct.', 'rotationAngleRequired' => 'An angle of rotation is required to rotate the image.', 'invalidPath' => 'The path to the image is not correct.', @@ -33,4 +32,7 @@ 'saveFailed' => 'Unable to save the image. Please make sure the image and file directory are writable.', 'invalidDirection' => 'Flip direction can be only "vertical" or "horizontal". Given: "{0}"', 'exifNotSupported' => 'Reading EXIF data is not supported by this PHP installation.', + + // @deprecated + 'libPathInvalid' => 'The path to your image library is not correct. Please set the correct path in your image preferences. "{0}"', ]; diff --git a/tests/system/Images/ImageMagickHandlerTest.php b/tests/system/Images/ImageMagickHandlerTest.php index 14cb4bc57980..924095253e48 100644 --- a/tests/system/Images/ImageMagickHandlerTest.php +++ b/tests/system/Images/ImageMagickHandlerTest.php @@ -16,11 +16,9 @@ use CodeIgniter\Config\Services; use CodeIgniter\Images\Exceptions\ImageException; use CodeIgniter\Images\Handlers\BaseHandler; -use CodeIgniter\Images\Handlers\ImageMagickHandler; use CodeIgniter\Test\CIUnitTestCase; use Config\Images; use Imagick; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RequiresPhpExtension; @@ -46,6 +44,10 @@ final class ImageMagickHandlerTest extends CIUnitTestCase protected function setUp(): void { + if (! extension_loaded('imagick')) { + $this->markTestSkipped('The IMAGICK extension is not available.'); + } + $this->root = WRITEPATH . 'cache/'; // cleanup everything @@ -58,54 +60,10 @@ protected function setUp(): void $this->path = $this->origin . 'ci-logo.png'; // get our locally available `convert` - $config = new Images(); - $found = false; - - foreach ([ - '/usr/bin/convert', - trim((string) shell_exec('which convert')), - $config->libraryPath, - ] as $convert) { - if (is_file($convert)) { - $config->libraryPath = $convert; - - $found = true; - break; - } - } - - if (! $found) { - $this->markTestSkipped('Cannot test imagick as there is no available convert program.'); - } - + $config = new Images(); $this->handler = Services::image('imagick', $config, false); } - #[DataProvider('provideNonexistentLibraryPathTerminatesProcessing')] - public function testNonexistentLibraryPathTerminatesProcessing(string $path, string $invalidPath): void - { - $this->expectException(ImageException::class); - $this->expectExceptionMessage(lang('Images.libPathInvalid', [$invalidPath])); - - $config = new Images(); - - $config->libraryPath = $path; - - new ImageMagickHandler($config); - } - - /** - * @return iterable> - */ - public static function provideNonexistentLibraryPathTerminatesProcessing(): iterable - { - yield 'empty string' => ['', '']; - - yield 'invalid file' => ['/var/log/convert', '/var/log/convert']; - - yield 'nonexistent file' => ['/var/www/file', '/var/www/file/convert']; - } - public function testGetVersion(): void { $version = $this->handler->getVersion(); @@ -458,13 +416,12 @@ public function testImageReorientLandscape(): void $this->handler->withFile($source); $this->handler->reorient(); + $this->handler->save($result); - $resource = imagecreatefromstring(file_get_contents($this->handler->getResource())); + $resource = imagecreatefromjpeg($result); $point = imagecolorat($resource, 0, 0); $rgb = imagecolorsforindex($resource, $point); - $this->handler->save($result); - $this->assertSame(['red' => 62, 'green' => 62, 'blue' => 62, 'alpha' => 0], $rgb); } } @@ -477,13 +434,12 @@ public function testImageReorientPortrait(): void $this->handler->withFile($source); $this->handler->reorient(); + $this->handler->save($result); - $resource = imagecreatefromstring(file_get_contents($this->handler->getResource())); + $resource = imagecreatefromjpeg($result); $point = imagecolorat($resource, 0, 0); $rgb = imagecolorsforindex($resource, $point); - $this->handler->save($result); - $this->assertSame(['red' => 62, 'green' => 62, 'blue' => 62, 'alpha' => 0], $rgb); } } diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index ae6797af7884..eb12d62068cb 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -57,7 +57,8 @@ Model Libraries ========= -**Email:** Added support for choosing the SMTP authorization method. You can change it via ``Config\Email::$SMTPAuthMethod`` option +**Email:** Added support for choosing the SMTP authorization method. You can change it via ``Config\Email::$SMTPAuthMethod`` option. +**Image:** The ``ImageMagickHandler`` has been rewritten to rely solely on the PHP ``imagick`` extension. Helpers and Functions ===================== @@ -70,7 +71,7 @@ Message Changes *************** - Added ``Email.invalidSMTPAuthMethod`` and ``Email.failureSMTPAuthMethod`` -- Deprecated ``Email.failedSMTPLogin`` +- Deprecated ``Email.failedSMTPLogin`` and ``Image.libPathInvalid`` ******* Changes @@ -80,6 +81,10 @@ Changes Deprecations ************ +- **Image:** + - The config property ``Config\Image::libraryPath`` has been deprecated. No longer used. + - The exception method ``CodeIgniter\Images\Exceptions\ImageException::forInvalidImageLibraryPath`` has been deprecated. No longer used. + ********** Bugs Fixed ********** diff --git a/user_guide_src/source/libraries/images.rst b/user_guide_src/source/libraries/images.rst index a439c3321a0b..f263eb173d06 100644 --- a/user_guide_src/source/libraries/images.rst +++ b/user_guide_src/source/libraries/images.rst @@ -36,9 +36,6 @@ The available Handlers are as follows: - ``gd`` The GD/GD2 image library - ``imagick`` The ImageMagick library. -If using the ImageMagick library, you must set the path to the library on your -server in **app/Config/Images.php**. - .. note:: The ImageMagick handler requires the imagick extension. ******************* @@ -263,6 +260,3 @@ The possible options that are recognized are as follows: - ``vOffset`` Additional offset on the y axis, in pixels - ``fontPath`` The full server path to the TTF font you wish to use. System font will be used if none is given. - ``fontSize`` The font size to use. When using the GD handler with the system font, valid values are between ``1`` to ``5``. - -.. note:: The ImageMagick driver does not recognize full server path for fontPath. Instead, simply provide the - name of one of the installed system fonts that you wish to use, i.e., Calibri. diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 6e6aa7de51fc..96d04deeddc6 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 146 errors +# total 144 errors parameters: ignoreErrors: @@ -212,11 +212,6 @@ parameters: count: 2 path: ../../tests/system/Images/GDHandlerTest.php - - - message: '#^Parameter \#1 \$filename of function file_get_contents expects string, resource given\.$#' - count: 2 - path: ../../tests/system/Images/ImageMagickHandlerTest.php - - message: '#^Parameter \#2 \$message of method CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:handle\(\) expects string, stdClass given\.$#' count: 1 diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index ea06afa8b720..f9d874b1855f 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -1,4 +1,4 @@ -# total 251 errors +# total 243 errors parameters: ignoreErrors: @@ -282,11 +282,6 @@ parameters: count: 2 path: ../../system/Images/Handlers/BaseHandler.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 8 - path: ../../system/Images/Handlers/ImageMagickHandler.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 316b3203b5a8..1f7ef749cff2 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3334 errors +# total 3314 errors includes: - argument.type.neon - assign.propertyType.neon diff --git a/utils/phpstan-baseline/method.childReturnType.neon b/utils/phpstan-baseline/method.childReturnType.neon index f66d38d4d6e4..e1e241985912 100644 --- a/utils/phpstan-baseline/method.childReturnType.neon +++ b/utils/phpstan-baseline/method.childReturnType.neon @@ -1,4 +1,4 @@ -# total 37 errors +# total 36 errors parameters: ignoreErrors: @@ -142,11 +142,6 @@ parameters: count: 1 path: ../../system/Images/Handlers/ImageMagickHandler.php - - - message: '#^Return type \(bool\|CodeIgniter\\Images\\Handlers\\ImageMagickHandler\) of method CodeIgniter\\Images\\Handlers\\ImageMagickHandler\:\:_crop\(\) should be covariant with return type \(\$this\(CodeIgniter\\Images\\Handlers\\BaseHandler\)\) of method CodeIgniter\\Images\\Handlers\\BaseHandler\:\:_crop\(\)$#' - count: 1 - path: ../../system/Images/Handlers/ImageMagickHandler.php - - message: '#^Return type \(array\|bool\|float\|int\|object\|string\|null\) of method CodeIgniter\\Model\:\:__call\(\) should be covariant with return type \(\$this\(CodeIgniter\\BaseModel\)\|null\) of method CodeIgniter\\BaseModel\:\:__call\(\)$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 5dfa442af4ab..9d13bf8e6299 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1582 errors +# total 1581 errors parameters: ignoreErrors: @@ -4532,11 +4532,6 @@ parameters: count: 1 path: ../../system/Images/Handlers/BaseHandler.php - - - message: '#^Method CodeIgniter\\Images\\Handlers\\ImageMagickHandler\:\:process\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Images/Handlers/ImageMagickHandler.php - - message: '#^Method CodeIgniter\\Images\\Image\:\:getProperties\(\) return type has no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/nullCoalesce.property.neon b/utils/phpstan-baseline/nullCoalesce.property.neon index 6caf5f23ef1d..05d64f32e0a0 100644 --- a/utils/phpstan-baseline/nullCoalesce.property.neon +++ b/utils/phpstan-baseline/nullCoalesce.property.neon @@ -1,4 +1,4 @@ -# total 20 errors +# total 12 errors parameters: ignoreErrors: @@ -27,16 +27,6 @@ parameters: count: 1 path: ../../system/HTTP/URI.php - - - message: '#^Property CodeIgniter\\Images\\Handlers\\BaseHandler\:\:\$height \(int\) on left side of \?\? is not nullable\.$#' - count: 4 - path: ../../system/Images/Handlers/ImageMagickHandler.php - - - - message: '#^Property CodeIgniter\\Images\\Handlers\\BaseHandler\:\:\$width \(int\) on left side of \?\? is not nullable\.$#' - count: 4 - path: ../../system/Images/Handlers/ImageMagickHandler.php - - message: '#^Property CodeIgniter\\Throttle\\Throttler\:\:\$testTime \(int\) on left side of \?\? is not nullable\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.phpDocType.neon b/utils/phpstan-baseline/property.phpDocType.neon index dda215c49436..6e00157b9feb 100644 --- a/utils/phpstan-baseline/property.phpDocType.neon +++ b/utils/phpstan-baseline/property.phpDocType.neon @@ -163,7 +163,7 @@ parameters: path: ../../system/HTTP/IncomingRequest.php - - message: '#^PHPDoc type string\|null of property CodeIgniter\\Images\\Handlers\\ImageMagickHandler\:\:\$resource is not the same as PHPDoc type resource\|null of overridden property CodeIgniter\\Images\\Handlers\\BaseHandler\:\:\$resource\.$#' + message: '#^PHPDoc type Imagick\|null of property CodeIgniter\\Images\\Handlers\\ImageMagickHandler\:\:\$resource is not the same as PHPDoc type resource\|null of overridden property CodeIgniter\\Images\\Handlers\\BaseHandler\:\:\$resource\.$#' count: 1 path: ../../system/Images/Handlers/ImageMagickHandler.php