diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 69f1be05..9173050b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -7,7 +7,7 @@ permissions: jobs: run: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: @@ -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 diff --git a/src/Drivers/Imagick/Encoders/JxlEncoder.php b/src/Drivers/Imagick/Encoders/JxlEncoder.php new file mode 100644 index 00000000..5bbeb588 --- /dev/null +++ b/src/Drivers/Imagick/Encoders/JxlEncoder.php @@ -0,0 +1,52 @@ +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); + } + } +} diff --git a/src/Encoders/JxlEncoder.php b/src/Encoders/JxlEncoder.php new file mode 100644 index 00000000..8b34937c --- /dev/null +++ b/src/Encoders/JxlEncoder.php @@ -0,0 +1,26 @@ + 100) { + throw new InvalidArgumentException('Quality must be in range 0 to 100'); + } + } +} diff --git a/src/FileExtension.php b/src/FileExtension.php index 622b263a..a41c7d4f 100644 --- a/src/FileExtension.php +++ b/src/FileExtension.php @@ -32,6 +32,7 @@ enum FileExtension: string case JPX = 'jpx'; case HEIC = 'heic'; case HEIF = 'heif'; + case JXL = 'jxl'; case ICO = 'ico'; /** @@ -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, }; } diff --git a/src/Format.php b/src/Format.php index ba8a9090..8ff1d1aa 100644 --- a/src/Format.php +++ b/src/Format.php @@ -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; @@ -30,6 +31,7 @@ enum Format case ICO; case JP2; case JPEG; + case JXL; case PNG; case TIFF; case WEBP; @@ -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, diff --git a/src/MediaType.php b/src/MediaType.php index 56a090fa..e4c56d18 100644 --- a/src/MediaType.php +++ b/src/MediaType.php @@ -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'; @@ -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, }; diff --git a/tests/Unit/Drivers/Gd/DriverTest.php b/tests/Unit/Drivers/Gd/DriverTest.php index f4d80668..74fcc91a 100644 --- a/tests/Unit/Drivers/Gd/DriverTest.php +++ b/tests/Unit/Drivers/Gd/DriverTest.php @@ -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']; diff --git a/tests/Unit/Drivers/Imagick/DriverTest.php b/tests/Unit/Drivers/Imagick/DriverTest.php index 60450ef4..082037c6 100644 --- a/tests/Unit/Drivers/Imagick/DriverTest.php +++ b/tests/Unit/Drivers/Imagick/DriverTest.php @@ -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; @@ -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]; diff --git a/tests/Unit/Drivers/Imagick/Encoders/JxlEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/JxlEncoderTest.php new file mode 100644 index 00000000..82280a6e --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Encoders/JxlEncoderTest.php @@ -0,0 +1,34 @@ +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()); + } +} diff --git a/tests/Unit/FileExtensionTest.php b/tests/Unit/FileExtensionTest.php index 742c28ff..a8ffe6c9 100644 --- a/tests/Unit/FileExtensionTest.php +++ b/tests/Unit/FileExtensionTest.php @@ -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; @@ -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]; diff --git a/tests/Unit/FormatTest.php b/tests/Unit/FormatTest.php index c0fe3233..0cfe9161 100644 --- a/tests/Unit/FormatTest.php +++ b/tests/Unit/FormatTest.php @@ -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; @@ -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; @@ -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; @@ -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; diff --git a/tests/Unit/MediaTypeTest.php b/tests/Unit/MediaTypeTest.php index 1e041a02..308ff722 100644 --- a/tests/Unit/MediaTypeTest.php +++ b/tests/Unit/MediaTypeTest.php @@ -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, @@ -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];