Skip to content
Merged
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
8 changes: 4 additions & 4 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ permissions:

jobs:
run:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -35,15 +35,15 @@ jobs:
sudo apt remove -y imagemagick imagemagick-6-common libmagic-dev
sudo apt update --allow-releaseinfo-change
sudo apt update
sudo apt install -y libjpeg-dev libgif-dev libtiff-dev libpng-dev libwebp-dev libavif-dev libheif-dev libraqm-dev libmagickwand-dev
sudo apt install -y libjpeg-dev libgif-dev libtiff-dev libpng-dev libwebp-dev libavif-dev libheif-dev libheif-plugin-x265 libheif-plugin-aomenc libheif-plugin-aomdec libheif-plugin-libde265 libjxl-dev libraqm-dev libmagickwand-dev

- name: Cache ImageMagick
uses: actions/cache@v6
id: cache-imagemagick
with:
path: /home/runner/im/imagemagick-${{ matrix.imagemagick }}
key: ${{ runner.os }}-ImageMagick-${{ matrix.imagemagick }}-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-ImageMagick-${{ matrix.imagemagick }}-
key: ${{ runner.os }}-ImageMagick-jxl1-${{ matrix.imagemagick }}-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-ImageMagick-jxl1-${{ matrix.imagemagick }}-

- name: Check ImageMagick cache exists
uses: andstor/file-existence-action@v3
Expand Down
52 changes: 52 additions & 0 deletions src/Drivers/Imagick/Encoders/JxlEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Intervention\Image\Drivers\Imagick\Encoders;

use ImagickException;
use Intervention\Image\Drivers\Imagick\Modifiers\StripMetaModifier;
use Intervention\Image\EncodedImage;
use Intervention\Image\Encoders\JxlEncoder as GenericJxlEncoder;
use Intervention\Image\Exceptions\EncoderException;
use Intervention\Image\Interfaces\EncodedImageInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Exceptions\StreamException;
use Intervention\Image\Exceptions\ImageException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Exceptions\StateException;

class JxlEncoder extends GenericJxlEncoder implements SpecializedInterface
{
/**
* @throws InvalidArgumentException
* @throws StreamException
* @throws StateException
* @throws EncoderException
*/
public function encode(ImageInterface $image): EncodedImageInterface
{
$format = 'JXL';

// strip meta data
if ($this->strip || (is_null($this->strip) && $this->driver()->config()->strip)) {
$image->modify(new StripMetaModifier());
}

try {
$imagick = clone $image->core()->native();
$imagick->setFormat($format);
$imagick->setImageFormat($format);
$imagick->setCompressionQuality($this->quality);
$imagick->setImageCompressionQuality($this->quality);

$result = new EncodedImage($imagick->getImagesBlob(), 'image/jxl');
$imagick->clear();

return $result;
} catch (ImagickException | ImageException $e) {
throw new EncoderException('Failed to encode jxl format', previous: $e);
}
}
}
26 changes: 26 additions & 0 deletions src/Encoders/JxlEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Intervention\Image\Encoders;

use Intervention\Image\Drivers\SpecializableEncoder;
use Intervention\Image\Exceptions\InvalidArgumentException;

class JxlEncoder extends SpecializableEncoder
{
/**
* Create new encoder object.
*
* @param null|bool $strip Strip EXIF metadata
* @throws InvalidArgumentException
*/
public function __construct(
public int $quality = self::DEFAULT_QUALITY,
public ?bool $strip = null,
) {
if ($quality < 0 || $quality > 100) {
throw new InvalidArgumentException('Quality must be in range 0 to 100');
}
}
}
2 changes: 2 additions & 0 deletions src/FileExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ enum FileExtension: string
case JPX = 'jpx';
case HEIC = 'heic';
case HEIF = 'heif';
case JXL = 'jxl';
case ICO = 'ico';

/**
Expand Down Expand Up @@ -120,6 +121,7 @@ public function format(): Format
self::JPX => Format::JP2,
self::HEIC,
self::HEIF => Format::HEIC,
self::JXL => Format::JXL,
self::ICO => Format::ICO,
};
}
Expand Down
3 changes: 3 additions & 0 deletions src/Format.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Intervention\Image\Encoders\IcoEncoder;
use Intervention\Image\Encoders\Jpeg2000Encoder;
use Intervention\Image\Encoders\JpegEncoder;
use Intervention\Image\Encoders\JxlEncoder;
use Intervention\Image\Encoders\PngEncoder;
use Intervention\Image\Encoders\TiffEncoder;
use Intervention\Image\Encoders\WebpEncoder;
Expand All @@ -30,6 +31,7 @@ enum Format
case ICO;
case JP2;
case JPEG;
case JXL;
case PNG;
case TIFF;
case WEBP;
Expand Down Expand Up @@ -157,6 +159,7 @@ public function encoder(mixed ...$options): EncoderInterface
self::ICO => IcoEncoder::class,
self::JP2 => Jpeg2000Encoder::class,
self::JPEG => JpegEncoder::class,
self::JXL => JxlEncoder::class,
self::PNG => PngEncoder::class,
self::TIFF => TiffEncoder::class,
self::WEBP => WebpEncoder::class,
Expand Down
4 changes: 4 additions & 0 deletions src/MediaType.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ enum MediaType: string
case IMAGE_HEIC = 'image/heic';
case IMAGE_X_HEIC = 'image/x-heic';
case IMAGE_HEIF = 'image/heif';
case IMAGE_JXL = 'image/jxl';
case IMAGE_X_JXL = 'image/x-jxl';
case IMAGE_X_ICON = 'image/x-icon';
case IMAGE_VND_MICROSOFT_ICON = 'image/vnd.microsoft.icon';

Expand Down Expand Up @@ -133,6 +135,8 @@ public function format(): Format
self::IMAGE_HEIF,
self::IMAGE_HEIC,
self::IMAGE_X_HEIC => Format::HEIC,
self::IMAGE_JXL,
self::IMAGE_X_JXL => Format::JXL,
self::IMAGE_X_ICON,
self::IMAGE_VND_MICROSOFT_ICON => Format::ICO,
};
Expand Down
8 changes: 8 additions & 0 deletions tests/Unit/Drivers/Gd/DriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ public static function supportsDataProvider(): Generator
yield [false, 'image/heic'];
yield [false, 'image/heif'];

yield [false, Format::JXL];
yield [false, MediaType::IMAGE_JXL];
yield [false, MediaType::IMAGE_X_JXL];
yield [false, FileExtension::JXL];
yield [false, 'jxl'];
yield [false, 'image/jxl'];
yield [false, 'image/x-jxl'];

yield [false, 'tga'];
yield [false, 'image/tga'];
yield [false, 'image/x-targa'];
Expand Down
16 changes: 16 additions & 0 deletions tests/Unit/Drivers/Imagick/DriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Intervention\Image\Tests\Unit\Drivers\Imagick;

use Generator;
use Imagick;
use Intervention\Image\Analyzers\WidthAnalyzer as GenericWidthAnalyzer;
use Intervention\Image\Decoders\FilePathImageDecoder as GenericFilePathImageDecoder;
use Intervention\Image\Encoders\PngEncoder as GenericPngEncoder;
Expand Down Expand Up @@ -106,6 +107,21 @@ public function testSupports(bool $result, mixed $identifier): void
$this->assertEquals($result, $this->driver->supports($identifier));
}

public function testSupportsJxl(): void
{
if (Imagick::queryFormats('JXL') === []) {
$this->markTestSkipped('ImageMagick was built without JXL (libjxl) support');
}

$this->assertTrue($this->driver->supports(Format::JXL));
$this->assertTrue($this->driver->supports(MediaType::IMAGE_JXL));
$this->assertTrue($this->driver->supports(MediaType::IMAGE_X_JXL));
$this->assertTrue($this->driver->supports(FileExtension::JXL));
$this->assertTrue($this->driver->supports('jxl'));
$this->assertTrue($this->driver->supports('image/jxl'));
$this->assertTrue($this->driver->supports('image/x-jxl'));
}

public static function supportsDataProvider(): Generator
{
yield [true, Format::JPEG];
Expand Down
34 changes: 34 additions & 0 deletions tests/Unit/Drivers/Imagick/Encoders/JxlEncoderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Intervention\Image\Tests\Unit\Drivers\Imagick\Encoders;

use Imagick;
use Intervention\Image\Drivers\Imagick\Driver;
use Intervention\Image\Drivers\Imagick\Encoders\JxlEncoder;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use Intervention\Image\Tests\ImagickTestCase;

#[RequiresPhpExtension('imagick')]
#[CoversClass(JxlEncoder::class)]
final class JxlEncoderTest extends ImagickTestCase
{
protected function setUp(): void
{
if (Imagick::queryFormats('JXL') === []) {
$this->markTestSkipped('ImageMagick was built without JXL (libjxl) support');
}
}

public function testEncode(): void
{
$image = $this->createTestImage(3, 2);
$encoder = new JxlEncoder(75);
$encoder->setDriver(new Driver());
$result = $encoder->encode($image);
$this->assertMediaType(['image/jxl', 'image/x-jxl'], $result);
$this->assertEquals('image/jxl', $result->mimetype());
}
}
7 changes: 7 additions & 0 deletions tests/Unit/FileExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ public function testFormatHeic(): void
$this->assertEquals(Format::HEIC, $ext->format());
}

public function testFormatJxl(): void
{
$ext = FileExtension::JXL;
$this->assertEquals(Format::JXL, $ext->format());
}

public function testFormatIco(): void
{
$ext = FileExtension::ICO;
Expand Down Expand Up @@ -168,6 +174,7 @@ public static function mediaTypesDataProvider(): Generator
yield [FileExtension::JP2, 4, MediaType::IMAGE_JP2];
yield [FileExtension::HEIC, 3, MediaType::IMAGE_HEIC];
yield [FileExtension::HEIF, 3, MediaType::IMAGE_HEIC];
yield [FileExtension::JXL, 2, MediaType::IMAGE_JXL];
yield [FileExtension::JPF, 4, MediaType::IMAGE_JP2];
yield [FileExtension::JPX, 4, MediaType::IMAGE_JP2];
yield [FileExtension::JPC, 4, MediaType::IMAGE_JP2];
Expand Down
27 changes: 27 additions & 0 deletions tests/Unit/FormatTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Intervention\Image\Encoders\IcoEncoder;
use Intervention\Image\Encoders\Jpeg2000Encoder;
use Intervention\Image\Encoders\JpegEncoder;
use Intervention\Image\Encoders\JxlEncoder;
use Intervention\Image\Encoders\PngEncoder;
use Intervention\Image\Encoders\TiffEncoder;
use Intervention\Image\Encoders\WebpEncoder;
Expand Down Expand Up @@ -147,6 +148,16 @@ public function testMediaTypesHeic(): void
$this->assertEquals(MediaType::IMAGE_HEIC, $format->mediaType());
}

public function testMediaTypesJxl(): void
{
$format = Format::JXL;
$mediaTypes = $format->mediaTypes();
$this->assertIsArray($mediaTypes);
$this->assertCount(2, $mediaTypes);

$this->assertEquals(MediaType::IMAGE_JXL, $format->mediaType());
}

public function testEncoderJpeg(): void
{
$format = Format::JPEG;
Expand Down Expand Up @@ -201,6 +212,12 @@ public function testEncoderHeic(): void
$this->assertInstanceOf(HeicEncoder::class, $format->encoder());
}

public function testEncoderJxl(): void
{
$format = Format::JXL;
$this->assertInstanceOf(JxlEncoder::class, $format->encoder());
}

public function testFileExtensionsJpeg(): void
{
$format = Format::JPEG;
Expand Down Expand Up @@ -291,6 +308,16 @@ public function testFileExtensionsHeic(): void
$this->assertEquals(FileExtension::HEIC, $format->fileExtension());
}

public function testFileExtensionsJxl(): void
{
$format = Format::JXL;
$extensions = $format->fileExtensions();
$this->assertIsArray($extensions);
$this->assertCount(1, $extensions);

$this->assertEquals(FileExtension::JXL, $format->fileExtension());
}

public function testMediaTypesIco(): void
{
$format = Format::ICO;
Expand Down
11 changes: 11 additions & 0 deletions tests/Unit/MediaTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,15 @@ public function testFormatHeic(): void
$this->assertEquals(Format::HEIC, $mime->format());
}

public function testFormatJxl(): void
{
$mime = MediaType::IMAGE_JXL;
$this->assertEquals(Format::JXL, $mime->format());

$mime = MediaType::IMAGE_X_JXL;
$this->assertEquals(Format::JXL, $mime->format());
}

#[DataProvider('fileExtensionsDataProvider')]
public function testFileExtensions(
MediaType $mediaType,
Expand Down Expand Up @@ -215,6 +224,8 @@ public static function fileExtensionsDataProvider(): Generator
yield [MediaType::IMAGE_HEIC, 2, FileExtension::HEIC];
yield [MediaType::IMAGE_X_HEIC, 2, FileExtension::HEIC];
yield [MediaType::IMAGE_HEIF, 2, FileExtension::HEIC];
yield [MediaType::IMAGE_JXL, 1, FileExtension::JXL];
yield [MediaType::IMAGE_X_JXL, 1, FileExtension::JXL];
yield [MediaType::IMAGE_X_JP2_CODESTREAM, 9, FileExtension::JP2];
yield [MediaType::IMAGE_X_ICON, 1, FileExtension::ICO];
yield [MediaType::IMAGE_VND_MICROSOFT_ICON, 1, FileExtension::ICO];
Expand Down