Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/reusable-phpunit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,16 @@ jobs:
if: ${{ contains(inputs.extra-extensions, 'imagick') }}
run: |
sudo apt-get update
sudo apt-get install -y imagemagick libmagickwand-dev ghostscript poppler-data libjbig2dec0:amd64 libopenjp2-7:amd64
sudo apt-get install -y ghostscript poppler-data libmagickwand-dev

# Install ImageMagick 7 with AVIF rw+ support (vintagesucks/imagemagick-deb)
RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/vintagesucks/imagemagick-deb/releases/latest)
mkdir -p /tmp/imagemagick-debs
while IFS= read -r url; do
curl -fsSL "$url" -o "/tmp/imagemagick-debs/$(basename "$url")"
done < <(echo "$RELEASE_JSON" | jq -r '.assets[] | select(.name | contains("noble_amd64")) | .browser_download_url')
sudo dpkg -i /tmp/imagemagick-debs/*.deb || true
sudo apt-get install -f -y

- name: Checkout base branch for PR
if: github.event_name == 'pull_request'
Expand Down
1 change: 1 addition & 0 deletions app/Config/Mimes.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ class Mimes
'image/x-png',
],
'webp' => 'image/webp',
'avif' => 'image/avif',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'css' => [
Expand Down
2 changes: 1 addition & 1 deletion app/Config/Publisher.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ class Publisher extends BasePublisher
*/
public $restrictions = [
ROOTPATH => '*',
FCPATH => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i',
FCPATH => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|avif|bmp|ico|svg)$#i',
];
}
2 changes: 1 addition & 1 deletion system/Config/Publisher.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class Publisher extends BaseConfig
*/
public $restrictions = [
ROOTPATH => '*',
FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i',
FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|avif|bmp|ico|svg)$#i',
];

/**
Expand Down
1 change: 1 addition & 0 deletions system/Images/Handlers/BaseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ abstract class BaseHandler implements ImageHandlerInterface
protected $supportTransparency = [
IMAGETYPE_PNG,
IMAGETYPE_WEBP,
IMAGETYPE_AVIF,
];

/**
Expand Down
21 changes: 19 additions & 2 deletions system/Images/Handlers/GDHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ protected function process(string $action)

$dest = $create($this->width, $this->height);

// for png and webp we can actually preserve transparency
// for png, webp and avif we can actually preserve transparency
if (in_array($this->image()->imageType, $this->supportTransparency, true)) {
imagealphablending($dest, false);
imagesavealpha($dest, true);
Expand Down Expand Up @@ -222,7 +222,7 @@ public function save(?string $target = null, int $quality = 90): bool

$this->ensureResource();

// for png and webp we can actually preserve transparency
// for png, webp and avif we can actually preserve transparency
if (in_array($this->image()->imageType, $this->supportTransparency, true)) {
imagepalettetotruecolor($this->resource);
imagealphablending($this->resource, false);
Expand Down Expand Up @@ -270,6 +270,16 @@ public function save(?string $target = null, int $quality = 90): bool
}
break;

case IMAGETYPE_AVIF:
if (! function_exists('imageavif')) {
throw ImageException::forInvalidImageCreate(lang('Images.avifNotSupported'));
}

if (! @imageavif($this->resource, $target, $quality)) {
throw ImageException::forSaveFailed();
}
break;

default:
throw ImageException::forInvalidImageCreate();
}
Expand Down Expand Up @@ -361,6 +371,13 @@ protected function getImageResource(string $path, int $imageType)

return imagecreatefromwebp($path);

case IMAGETYPE_AVIF:
if (! function_exists('imagecreatefromavif')) {
throw ImageException::forInvalidImageCreate(lang('Images.avifNotSupported'));
}

return imagecreatefromavif($path);

default:
throw ImageException::forInvalidImageCreate('Ima');
}
Expand Down
34 changes: 32 additions & 2 deletions system/Images/Handlers/ImageMagickHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,38 @@ protected function supportedFormatCheck()
return;
}

if ($this->image()->imageType === IMAGETYPE_WEBP && ! in_array('WEBP', Imagick::queryFormats(), true)) {
throw ImageException::forInvalidImageCreate(lang('images.webpNotSupported'));
$supported = Imagick::queryFormats();

switch ($this->image()->imageType) {
case IMAGETYPE_GIF:
if (! in_array('GIF', $supported, true)) {
throw ImageException::forInvalidImageCreate(lang('Images.gifNotSupported'));
}
break;

case IMAGETYPE_JPEG:
if (! in_array('JPEG', $supported, true)) {
throw ImageException::forInvalidImageCreate(lang('Images.jpgNotSupported'));
}
break;

case IMAGETYPE_PNG:
if (! in_array('PNG', $supported, true)) {
throw ImageException::forInvalidImageCreate(lang('Images.pngNotSupported'));
}
break;

case IMAGETYPE_WEBP:
if (! in_array('WEBP', $supported, true)) {
throw ImageException::forInvalidImageCreate(lang('Images.webpNotSupported'));
}
break;

case IMAGETYPE_AVIF:
if (! in_array('AVIF', $supported, true)) {
throw ImageException::forInvalidImageCreate(lang('Images.avifNotSupported'));
}
break;
}
}

Expand Down
1 change: 1 addition & 0 deletions system/Images/Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public function getProperties(bool $return = false)
IMAGETYPE_JPEG => 'jpeg',
IMAGETYPE_PNG => 'png',
IMAGETYPE_WEBP => 'webp',
IMAGETYPE_AVIF => 'avif',
];

$mime = 'image/' . ($types[$vals[2]] ?? 'jpg');
Expand Down
1 change: 1 addition & 0 deletions system/Language/en/Images.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
'jpgNotSupported' => 'JPG images are not supported.',
'pngNotSupported' => 'PNG images are not supported.',
'webpNotSupported' => 'WEBP images are not supported.',
'avifNotSupported' => 'AVIF images are not supported.',
'fileNotSupported' => 'The supplied file is not a supported image type.',
'unsupportedImageCreate' => 'Your server does not support the required functionality to process this type of image.',
'jpgOrPngRequired' => 'The image resize protocol specified in your preferences only works with JPEG or PNG image types.',
Expand Down
Binary file added tests/_support/Images/ci-logo.avif
Binary file not shown.
43 changes: 34 additions & 9 deletions tests/system/Images/GDHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -319,10 +319,14 @@ public function testMoreText(): void

public function testImageCreation(): void
{
foreach (['gif', 'jpeg', 'png', 'webp'] as $type) {
foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) {
if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.');
$this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.');
}
if ($type === 'avif' && ! function_exists('imagecreatefromavif')) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.');
}

$this->handler->withFile($this->origin . 'ci-logo.' . $type);
Expand All @@ -334,10 +338,14 @@ public function testImageCreation(): void

public function testImageCopy(): void
{
foreach (['gif', 'jpeg', 'png', 'webp'] as $type) {
foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) {
if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.');
$this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.');
}
if ($type === 'avif' && ! function_exists('imagecreatefromavif')) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.');
}

$this->handler->withFile($this->origin . 'ci-logo.' . $type);
Expand All @@ -353,7 +361,7 @@ public function testImageCopy(): void

public function testImageCopyWithNoTargetAndMaxQuality(): void
{
foreach (['gif', 'jpeg', 'png', 'webp'] as $type) {
foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) {
$this->handler->withFile($this->origin . 'ci-logo.' . $type);
$this->handler->save(null, 100);
$this->assertFileExists($this->origin . 'ci-logo.' . $type);
Expand All @@ -367,10 +375,14 @@ public function testImageCopyWithNoTargetAndMaxQuality(): void

public function testImageCompressionGetResource(): void
{
foreach (['gif', 'jpeg', 'png', 'webp'] as $type) {
foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) {
if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.');
$this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.');
}
if ($type === 'avif' && ! function_exists('imagecreatefromavif')) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.');
}

$this->handler->withFile($this->origin . 'ci-logo.' . $type);
Expand All @@ -387,10 +399,14 @@ public function testImageCompressionGetResource(): void

public function testImageCompressionWithResource(): void
{
foreach (['gif', 'jpeg', 'png', 'webp'] as $type) {
foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) {
if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.');
$this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.');
}
if ($type === 'avif' && ! function_exists('imagecreatefromavif')) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.');
}

$this->handler->withFile($this->origin . 'ci-logo.' . $type)
Expand Down Expand Up @@ -423,6 +439,15 @@ public function testImageConvertPngToWebp(): void
$this->assertSame(IMAGETYPE_WEBP, exif_imagetype($saved));
}

public function testImageConvertPngToAvif(): void
{
$this->handler->withFile($this->origin . 'rocket.png');
$this->handler->convert(IMAGETYPE_AVIF);
$saved = $this->start . 'work/rocket.avif';
$this->handler->save($saved);
$this->assertSame(IMAGETYPE_AVIF, exif_imagetype($saved));
}

public function testImageReorientLandscape(): void
{
for ($i = 0; $i <= 8; $i++) {
Expand Down
50 changes: 41 additions & 9 deletions tests/system/Images/ImageMagickHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,14 @@ public function testMoreText(): void

public function testImageCreation(): void
{
foreach (['gif', 'jpeg', 'png', 'webp'] as $type) {
foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) {
if ($type === 'webp' && ! in_array('WEBP', Imagick::queryFormats(), true)) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.');
$this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.');
}
if ($type === 'avif' && ! in_array('AVIF', Imagick::queryFormats(), true)) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.');
}

$this->handler->withFile($this->origin . 'ci-logo.' . $type);
Expand All @@ -328,10 +332,14 @@ public function testImageCreation(): void

public function testImageCopy(): void
{
foreach (['gif', 'jpeg', 'png', 'webp'] as $type) {
foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) {
if ($type === 'webp' && ! in_array('WEBP', Imagick::queryFormats(), true)) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.');
$this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.');
}
if ($type === 'avif' && ! in_array('AVIF', Imagick::queryFormats(), true)) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.');
}

$this->handler->withFile($this->origin . 'ci-logo.' . $type);
Expand All @@ -347,7 +355,7 @@ public function testImageCopy(): void

public function testImageCopyWithNoTargetAndMaxQuality(): void
{
foreach (['gif', 'jpeg', 'png', 'webp'] as $type) {
foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) {
$this->handler->withFile($this->origin . 'ci-logo.' . $type);
$this->handler->save(null, 100);
$this->assertFileExists($this->origin . 'ci-logo.' . $type);
Expand All @@ -361,10 +369,14 @@ public function testImageCopyWithNoTargetAndMaxQuality(): void

public function testImageCompressionGetResource(): void
{
foreach (['gif', 'jpeg', 'png', 'webp'] as $type) {
foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) {
if ($type === 'webp' && ! in_array('WEBP', Imagick::queryFormats(), true)) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.');
$this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.');
}
if ($type === 'avif' && ! in_array('AVIF', Imagick::queryFormats(), true)) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.');
}

$this->handler->withFile($this->origin . 'ci-logo.' . $type);
Expand All @@ -381,10 +393,14 @@ public function testImageCompressionGetResource(): void

public function testImageCompressionWithResource(): void
{
foreach (['gif', 'jpeg', 'png', 'webp'] as $type) {
foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) {
if ($type === 'webp' && ! in_array('WEBP', Imagick::queryFormats(), true)) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.');
$this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.');
}
if ($type === 'avif' && ! in_array('AVIF', Imagick::queryFormats(), true)) {
$this->expectException(ImageException::class);
$this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.');
}

$this->handler->withFile($this->origin . 'ci-logo.' . $type)
Expand All @@ -408,6 +424,22 @@ public function testImageConvert(): void
$this->assertSame(IMAGETYPE_PNG, exif_imagetype($this->root . 'ci-logo.png'));
}

public function testImageConvertPngToWebp(): void
{
$this->handler->withFile($this->origin . 'ci-logo.png');
$this->handler->convert(IMAGETYPE_WEBP);
$this->handler->save($this->root . 'ci-logo.webp');
$this->assertSame(IMAGETYPE_WEBP, exif_imagetype($this->root . 'ci-logo.webp'));
}

public function testImageConvertPngToAvif(): void
{
$this->handler->withFile($this->origin . 'ci-logo.png');
$this->handler->convert(IMAGETYPE_AVIF);
$this->handler->save($this->root . 'ci-logo.avif');
$this->assertSame(IMAGETYPE_AVIF, exif_imagetype($this->root . 'ci-logo.avif'));
}

public function testImageReorientLandscape(): void
{
for ($i = 0; $i <= 8; $i++) {
Expand Down
1 change: 1 addition & 0 deletions user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ Libraries
- **Context**: This new feature allows you to easily set and retrieve normal or hidden contextual data for the current request. See :ref:`Context <context>` for details.
- **Logging:** Log handlers now receive the full context array as a third argument to ``handle()``. When ``$logGlobalContext`` is enabled, the CI global context is available under the ``HandlerInterface::GLOBAL_CONTEXT_KEY`` key. Built-in handlers append it to the log output; custom handlers can use it for structured logging.
- **Logging:** Added :ref:`per-call context logging <logging-per-call-context>` with three new ``Config\Logger`` options (``$logContext``, ``$logContextTrace``, ``$logContextUsedKeys``). Per PSR-3, a ``Throwable`` in the ``exception`` context key is automatically normalized to a meaningful array. All options default to ``false``.
- **Images:**: Added support for the AVIF file format.

Helpers and Functions
=====================
Expand Down
Loading