1
0
mirror of https://github.com/Intervention/image.git synced 2025-08-06 13:56:30 +02:00

Add Encoder Options for progressive Jpeg & interlaced GIF format

This commit is contained in:
Oliver Vogel
2024-03-27 15:30:28 +01:00
committed by GitHub
parent c261654599
commit d982359ab8
18 changed files with 203 additions and 39 deletions

View File

@@ -21,7 +21,7 @@
"require": { "require": {
"php": "^8.1", "php": "^8.1",
"ext-mbstring": "*", "ext-mbstring": "*",
"intervention/gif": "^4.0.1" "intervention/gif": "^4.1"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^10.0", "phpunit/phpunit": "^10.0",

View File

@@ -23,7 +23,9 @@ class GifEncoder extends GenericGifEncoder implements SpecializedInterface
$gd = $image->core()->native(); $gd = $image->core()->native();
$data = $this->buffered(function () use ($gd) { $data = $this->buffered(function () use ($gd) {
imageinterlace($gd, $this->interlaced);
imagegif($gd); imagegif($gd);
imageinterlace($gd, false);
}); });
return new EncodedImage($data, 'image/gif'); return new EncodedImage($data, 'image/gif');
@@ -41,8 +43,9 @@ class GifEncoder extends GenericGifEncoder implements SpecializedInterface
foreach ($image as $frame) { foreach ($image as $frame) {
$builder->addFrame( $builder->addFrame(
(string) $this->encode($frame->toImage($image->driver())), source: (string) $this->encode($frame->toImage($image->driver())),
$frame->delay() delay: $frame->delay(),
interlaced: $this->interlaced
); );
} }

View File

@@ -17,6 +17,7 @@ class JpegEncoder extends GenericJpegEncoder implements SpecializedInterface
$output = Cloner::cloneBlended($image->core()->native(), background: $image->blendingColor()); $output = Cloner::cloneBlended($image->core()->native(), background: $image->blendingColor());
$data = $this->buffered(function () use ($output) { $data = $this->buffered(function () use ($output) {
imageinterlace($output, $this->progressive);
imagejpeg($output, null, $this->quality); imagejpeg($output, null, $this->quality);
}); });

View File

@@ -24,6 +24,10 @@ class GifEncoder extends GenericGifEncoder implements SpecializedInterface
$imagick->setCompression($compression); $imagick->setCompression($compression);
$imagick->setImageCompression($compression); $imagick->setImageCompression($compression);
if ($this->interlaced) {
$imagick->setInterlaceScheme(Imagick::INTERLACE_LINE);
}
return new EncodedImage($imagick->getImagesBlob(), 'image/gif'); return new EncodedImage($imagick->getImagesBlob(), 'image/gif');
} }
} }

View File

@@ -37,6 +37,10 @@ class JpegEncoder extends GenericJpegEncoder implements SpecializedInterface
$imagick->setImageCompressionQuality($this->quality); $imagick->setImageCompressionQuality($this->quality);
$imagick->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE); $imagick->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
if ($this->progressive) {
$imagick->setInterlaceScheme(Imagick::INTERLACE_PLANE);
}
return new EncodedImage($imagick->getImagesBlob(), 'image/jpeg'); return new EncodedImage($imagick->getImagesBlob(), 'image/jpeg');
} }
} }

View File

@@ -11,17 +11,17 @@ use Intervention\Image\Interfaces\ImageInterface;
class FileExtensionEncoder extends AutoEncoder class FileExtensionEncoder extends AutoEncoder
{ {
protected array $options = [];
/** /**
* Create new encoder instance to encode to format of given file extension * Create new encoder instance to encode to format of given file extension
* *
* @param null|string $extension Target file extension for example "png" * @param null|string $extension Target file extension for example "png"
* @param int $quality
* @return void * @return void
*/ */
public function __construct( public function __construct(public ?string $extension = null, mixed ...$options)
public ?string $extension = null, {
public int $quality = self::DEFAULT_QUALITY $this->options = $options;
) {
} }
/** /**
@@ -52,15 +52,15 @@ class FileExtensionEncoder extends AutoEncoder
} }
return match (strtolower($extension)) { return match (strtolower($extension)) {
'webp' => new WebpEncoder(quality: $this->quality), 'webp' => new WebpEncoder(...$this->options),
'avif' => new AvifEncoder(quality: $this->quality), 'avif' => new AvifEncoder(...$this->options),
'jpeg', 'jpg' => new JpegEncoder(quality: $this->quality), 'jpeg', 'jpg' => new JpegEncoder(...$this->options),
'bmp' => new BmpEncoder(), 'bmp' => new BmpEncoder(...$this->options),
'gif' => new GifEncoder(), 'gif' => new GifEncoder(...$this->options),
'png' => new PngEncoder(), 'png' => new PngEncoder(...$this->options),
'tiff', 'tif' => new TiffEncoder(quality: $this->quality), 'tiff', 'tif' => new TiffEncoder(...$this->options),
'jp2', 'j2k', 'jpf', 'jpm', 'jpg2', 'j2c', 'jpc', 'jpx' => new Jpeg2000Encoder(quality: $this->quality), 'jp2', 'j2k', 'jpf', 'jpm', 'jpg2', 'j2c', 'jpc', 'jpx' => new Jpeg2000Encoder(...$this->options),
'heic', 'heif' => new HeicEncoder(quality: $this->quality), 'heic', 'heif' => new HeicEncoder(...$this->options),
default => throw new EncoderException('No encoder found for file extension (' . $extension . ').'), default => throw new EncoderException('No encoder found for file extension (' . $extension . ').'),
}; };
} }

View File

@@ -13,14 +13,13 @@ class FilePathEncoder extends FileExtensionEncoder
* Create new encoder instance to encode to format of file extension in given path * Create new encoder instance to encode to format of file extension in given path
* *
* @param null|string $path * @param null|string $path
* @param int $quality
* @return void * @return void
*/ */
public function __construct(protected ?string $path = null, public int $quality = self::DEFAULT_QUALITY) public function __construct(protected ?string $path = null, mixed ...$options)
{ {
parent::__construct( parent::__construct(
is_null($path) ? $path : pathinfo($path, PATHINFO_EXTENSION), is_null($path) ? $path : pathinfo($path, PATHINFO_EXTENSION),
$quality ...$options
); );
} }

View File

@@ -8,7 +8,7 @@ use Intervention\Image\Drivers\SpecializableEncoder;
class GifEncoder extends SpecializableEncoder class GifEncoder extends SpecializableEncoder
{ {
public function __construct() public function __construct(public bool $interlaced = false)
{ {
} }
} }

View File

@@ -8,7 +8,9 @@ use Intervention\Image\Drivers\SpecializableEncoder;
class JpegEncoder extends SpecializableEncoder class JpegEncoder extends SpecializableEncoder
{ {
public function __construct(public int $quality = self::DEFAULT_QUALITY) public function __construct(
{ public int $quality = self::DEFAULT_QUALITY,
public bool $progressive = false
) {
} }
} }

View File

@@ -12,17 +12,17 @@ use Intervention\Image\Interfaces\ImageInterface;
class MediaTypeEncoder extends AbstractEncoder class MediaTypeEncoder extends AbstractEncoder
{ {
protected array $options = [];
/** /**
* Create new encoder instance * Create new encoder instance
* *
* @param null|string $mediaType Target media type for example "image/jpeg" * @param null|string $mediaType Target media type for example "image/jpeg"
* @param int $quality
* @return void * @return void
*/ */
public function __construct( public function __construct(public ?string $mediaType = null, mixed ...$options)
public ?string $mediaType = null, {
public int $quality = self::DEFAULT_QUALITY $this->options = $options;
) {
} }
/** /**
@@ -50,12 +50,12 @@ class MediaTypeEncoder extends AbstractEncoder
{ {
return match (strtolower($mediaType)) { return match (strtolower($mediaType)) {
'image/webp', 'image/webp',
'image/x-webp' => new WebpEncoder(quality: $this->quality), 'image/x-webp' => new WebpEncoder(...$this->options),
'image/avif', 'image/avif',
'image/x-avif' => new AvifEncoder(quality: $this->quality), 'image/x-avif' => new AvifEncoder(...$this->options),
'image/jpeg', 'image/jpeg',
'image/jpg', 'image/jpg',
'image/pjpeg' => new JpegEncoder(quality: $this->quality), 'image/pjpeg' => new JpegEncoder(...$this->options),
'image/bmp', 'image/bmp',
'image/ms-bmp', 'image/ms-bmp',
'image/x-bitmap', 'image/x-bitmap',
@@ -63,16 +63,16 @@ class MediaTypeEncoder extends AbstractEncoder
'image/x-ms-bmp', 'image/x-ms-bmp',
'image/x-win-bitmap', 'image/x-win-bitmap',
'image/x-windows-bmp', 'image/x-windows-bmp',
'image/x-xbitmap' => new BmpEncoder(), 'image/x-xbitmap' => new BmpEncoder(...$this->options),
'image/gif' => new GifEncoder(), 'image/gif' => new GifEncoder(...$this->options),
'image/png', 'image/png',
'image/x-png' => new PngEncoder(), 'image/x-png' => new PngEncoder(...$this->options),
'image/tiff' => new TiffEncoder(quality: $this->quality), 'image/tiff' => new TiffEncoder(...$this->options),
'image/jp2', 'image/jp2',
'image/jpx', 'image/jpx',
'image/jpm' => new Jpeg2000Encoder(quality: $this->quality), 'image/jpm' => new Jpeg2000Encoder(...$this->options),
'image/heic', 'image/heic',
'image/heif', => new HeicEncoder(quality: $this->quality), 'image/heif', => new HeicEncoder(...$this->options),
default => throw new EncoderException('No encoder found for media type (' . $mediaType . ').'), default => throw new EncoderException('No encoder found for media type (' . $mediaType . ').'),
}; };
} }

View File

@@ -299,7 +299,7 @@ final class Image implements ImageInterface
* *
* @see ImageInterface::save() * @see ImageInterface::save()
*/ */
public function save(?string $path = null, ...$options): ImageInterface public function save(?string $path = null, mixed ...$options): ImageInterface
{ {
$path = is_null($path) ? $this->origin()->filePath() : $path; $path = is_null($path) ? $this->origin()->filePath() : $path;

View File

@@ -88,7 +88,7 @@ interface ImageInterface extends IteratorAggregate, Countable
* @throws RuntimeException * @throws RuntimeException
* @return ImageInterface * @return ImageInterface
*/ */
public function save(?string $path = null, ...$options): self; public function save(?string $path = null, mixed ...$options): self;
/** /**
* Apply given modifier to current image * Apply given modifier to current image

View File

@@ -35,4 +35,22 @@ abstract class ImagickTestCase extends BaseTestCase
new Core($imagick) new Core($imagick)
); );
} }
public function createTestAnimation(): Image
{
$imagick = new Imagick();
$imagick->setFormat('gif');
for ($i = 0; $i < 3; $i++) {
$frame = new Imagick();
$frame->newImage(3, 2, new ImagickPixel('rgb(255, 0, 0)'), 'gif');
$frame->setImageDelay(10);
$imagick->addImage($frame);
}
return new Image(
new Driver(),
new Core($imagick)
);
}
} }

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Tests\Traits;
use Intervention\Image\Traits\CanBuildFilePointer;
trait CanDetectProgressiveJpeg
{
use CanBuildFilePointer;
/**
* Checks if the given image data is progressive encoded Jpeg format
*
* @param string $imagedata
* @return bool
*/
private function isProgressiveJpeg(string $imagedata): bool
{
$f = $this->buildFilePointer($imagedata);
while (!feof($f)) {
if (unpack('C', fread($f, 1))[1] !== 0xff) {
return false;
}
$blockType = unpack('C', fread($f, 1))[1];
switch (true) {
case $blockType == 0xd8:
case $blockType >= 0xd0 && $blockType <= 0xd7:
break;
case $blockType == 0xc0:
fclose($f);
return false;
case $blockType == 0xc2:
fclose($f);
return true;
case $blockType == 0xd9:
break 2;
default:
$blockSize = unpack('n', fread($f, 2))[1];
fseek($f, $blockSize - 2, SEEK_CUR);
break;
}
}
fclose($f);
return false;
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Intervention\Image\Tests\Unit\Drivers\Gd\Encoders; namespace Intervention\Image\Tests\Unit\Drivers\Gd\Encoders;
use Intervention\Gif\Decoder;
use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use Intervention\Image\Encoders\GifEncoder; use Intervention\Image\Encoders\GifEncoder;
@@ -20,5 +21,30 @@ final class GifEncoderTest extends GdTestCase
$encoder = new GifEncoder(); $encoder = new GifEncoder();
$result = $encoder->encode($image); $result = $encoder->encode($image);
$this->assertMediaType('image/gif', (string) $result); $this->assertMediaType('image/gif', (string) $result);
$this->assertFalse(
Decoder::decode((string) $result)->getFirstFrame()->getImageDescriptor()->isInterlaced()
);
}
public function testEncodeInterlaced(): void
{
$image = $this->createTestImage(3, 2);
$encoder = new GifEncoder(interlaced: true);
$result = $encoder->encode($image);
$this->assertMediaType('image/gif', (string) $result);
$this->assertTrue(
Decoder::decode((string) $result)->getFirstFrame()->getImageDescriptor()->isInterlaced()
);
}
public function testEncodeInterlacedAnimation(): void
{
$image = $this->createTestAnimation(3, 2);
$encoder = new GifEncoder(interlaced: true);
$result = $encoder->encode($image);
$this->assertMediaType('image/gif', (string) $result);
$this->assertTrue(
Decoder::decode((string) $result)->getFirstFrame()->getImageDescriptor()->isInterlaced()
);
} }
} }

View File

@@ -8,12 +8,15 @@ use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use Intervention\Image\Encoders\JpegEncoder; use Intervention\Image\Encoders\JpegEncoder;
use Intervention\Image\Tests\GdTestCase; use Intervention\Image\Tests\GdTestCase;
use Intervention\Image\Tests\Traits\CanDetectProgressiveJpeg;
#[RequiresPhpExtension('gd')] #[RequiresPhpExtension('gd')]
#[CoversClass(\Intervention\Image\Encoders\JpegEncoder::class)] #[CoversClass(\Intervention\Image\Encoders\JpegEncoder::class)]
#[CoversClass(\Intervention\Image\Drivers\Gd\Encoders\JpegEncoder::class)] #[CoversClass(\Intervention\Image\Drivers\Gd\Encoders\JpegEncoder::class)]
final class JpegEncoderTest extends GdTestCase final class JpegEncoderTest extends GdTestCase
{ {
use CanDetectProgressiveJpeg;
public function testEncode(): void public function testEncode(): void
{ {
$image = $this->createTestImage(3, 2); $image = $this->createTestImage(3, 2);
@@ -21,4 +24,13 @@ final class JpegEncoderTest extends GdTestCase
$result = $encoder->encode($image); $result = $encoder->encode($image);
$this->assertMediaType('image/jpeg', (string) $result); $this->assertMediaType('image/jpeg', (string) $result);
} }
public function testEncodeProgressive(): void
{
$image = $this->createTestImage(3, 2);
$encoder = new JpegEncoder(progressive: true);
$result = $encoder->encode($image);
$this->assertMediaType('image/jpeg', (string) $result);
$this->assertTrue($this->isProgressiveJpeg((string) $result));
}
} }

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Intervention\Image\Tests\Unit\Drivers\Imagick\Encoders; namespace Intervention\Image\Tests\Unit\Drivers\Imagick\Encoders;
use Intervention\Gif\Decoder;
use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use Intervention\Image\Encoders\GifEncoder; use Intervention\Image\Encoders\GifEncoder;
@@ -20,5 +21,30 @@ final class GifEncoderTest extends ImagickTestCase
$encoder = new GifEncoder(); $encoder = new GifEncoder();
$result = $encoder->encode($image); $result = $encoder->encode($image);
$this->assertMediaType('image/gif', (string) $result); $this->assertMediaType('image/gif', (string) $result);
$this->assertFalse(
Decoder::decode((string) $result)->getFirstFrame()->getImageDescriptor()->isInterlaced()
);
}
public function testEncodeInterlaced(): void
{
$image = $this->createTestImage(3, 2);
$encoder = new GifEncoder(interlaced: true);
$result = $encoder->encode($image);
$this->assertMediaType('image/gif', (string) $result);
$this->assertTrue(
Decoder::decode((string) $result)->getFirstFrame()->getImageDescriptor()->isInterlaced()
);
}
public function testEncodeInterlacedAnimation(): void
{
$image = $this->createTestAnimation();
$encoder = new GifEncoder(interlaced: true);
$result = $encoder->encode($image);
$this->assertMediaType('image/gif', (string) $result);
$this->assertTrue(
Decoder::decode((string) $result)->getFirstFrame()->getImageDescriptor()->isInterlaced()
);
} }
} }

View File

@@ -8,12 +8,15 @@ use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use Intervention\Image\Encoders\JpegEncoder; use Intervention\Image\Encoders\JpegEncoder;
use Intervention\Image\Tests\ImagickTestCase; use Intervention\Image\Tests\ImagickTestCase;
use Intervention\Image\Tests\Traits\CanDetectProgressiveJpeg;
#[RequiresPhpExtension('imagick')] #[RequiresPhpExtension('imagick')]
#[CoversClass(\Intervention\Image\Encoders\JpegEncoder::class)] #[CoversClass(\Intervention\Image\Encoders\JpegEncoder::class)]
#[CoversClass(\Intervention\Image\Drivers\Imagick\Encoders\JpegEncoder::class)] #[CoversClass(\Intervention\Image\Drivers\Imagick\Encoders\JpegEncoder::class)]
final class JpegEncoderTest extends ImagickTestCase final class JpegEncoderTest extends ImagickTestCase
{ {
use CanDetectProgressiveJpeg;
public function testEncode(): void public function testEncode(): void
{ {
$image = $this->createTestImage(3, 2); $image = $this->createTestImage(3, 2);
@@ -21,4 +24,13 @@ final class JpegEncoderTest extends ImagickTestCase
$result = $encoder->encode($image); $result = $encoder->encode($image);
$this->assertMediaType('image/jpeg', (string) $result); $this->assertMediaType('image/jpeg', (string) $result);
} }
public function testEncodeProgressive(): void
{
$image = $this->createTestImage(3, 2);
$encoder = new JpegEncoder(progressive: true);
$result = $encoder->encode($image);
$this->assertMediaType('image/jpeg', (string) $result);
$this->assertTrue($this->isProgressiveJpeg((string) $result));
}
} }