Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manipulating animated GIF's is now supported using Imagick. #234

Merged
merged 2 commits into from
Feb 16, 2024
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
170 changes: 109 additions & 61 deletions src/Drivers/Imagick/ImagickDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,17 @@ public function loadFile(string $path): static
$this->image = new Imagick($path);
$this->exif = $this->image->getImageProperties('exif:*');

if ($this->isAnimated()) {
$this->image = $this->image->coalesceImages();
}
return $this;
}

protected function isAnimated(): bool
{
return count($this->image) > 1;
}

public function getWidth(): int
{
return $this->image->getImageWidth();
Expand All @@ -90,14 +98,18 @@ public function getHeight(): int

public function brightness(int $brightness): static
{
$this->image->modulateImage(100 + $brightness, 100, 100);
foreach ($this->image as $image) {
$image->modulateImage(100 + $brightness, 100, 100);
}

return $this;
}

public function blur(int $blur): static
{
$this->image->blurImage(0.5 * $blur, 0.1 * $blur);
foreach ($this->image as $image) {
$image->blurImage(0.5 * $blur, 0.1 * $blur);
}

return $this;
}
Expand All @@ -115,7 +127,9 @@ public function fit(Fit $fit, ?int $desiredWidth = null, ?int $desiredHeight = n
$desiredHeight
);

$this->image->scaleImage($calculatedSize->width, $calculatedSize->height);
foreach ($this->image as $image) {
$image->scaleImage($calculatedSize->width, $calculatedSize->height);
}

if ($fit->shouldResizeCanvas()) {
$this->resizeCanvas($desiredWidth, $desiredHeight, AlignPosition::Center, false, null);
Expand All @@ -125,12 +139,13 @@ public function fit(Fit $fit, ?int $desiredWidth = null, ?int $desiredHeight = n
}

public function resizeCanvas(
?int $width = null,
?int $height = null,
?int $width = null,
?int $height = null,
?AlignPosition $position = null,
bool $relative = false,
?string $backgroundColor = null
): static {
bool $relative = false,
?string $backgroundColor = null
): static
{
$position ??= AlignPosition::Center;

$originalWidth = $this->getWidth();
Expand Down Expand Up @@ -211,17 +226,22 @@ public function pickColor(int $x, int $y, ColorFormat $colorFormat): mixed

public function save(?string $path = null): static
{
if (! $path) {
if (!$path) {
$path = $this->originalPath;
}

$extension = pathinfo($path, PATHINFO_EXTENSION);

if (! in_array(strtoupper($extension), Imagick::queryFormats('*'))) {
if (!in_array(strtoupper($extension), Imagick::queryFormats('*'))) {
throw UnsupportedImageFormat::make($extension);
}

$this->image->writeImage($path);
if ($this->isAnimated()) {
$image = $this->image->deconstructImages();
$image->writeImages($path, true);
} else {
$this->image->writeImage($path);
}

if ($this->optimize) {
$this->optimizerChain->optimize($path);
Expand All @@ -236,7 +256,7 @@ public function base64(string $imageFormat = 'jpeg', bool $prefixWithFormat = tr
$image->setFormat($imageFormat);

if ($prefixWithFormat) {
return 'data:image/'.$imageFormat.';base64,'.base64_encode($image->getImageBlob());
return 'data:image/' . $imageFormat . ';base64,' . base64_encode($image->getImageBlob());
}

return base64_encode($image->getImageBlob());
Expand All @@ -254,14 +274,18 @@ public function getSize(): Size

public function gamma(float $gamma): static
{
$this->image->gammaImage($gamma);
foreach ($this->image as $image) {
$image->gammaImage($gamma);
}

return $this;
}

public function contrast(float $level): static
{
$this->image->brightnessContrastImage(1, $level);
foreach ($this->image as $image) {
$image->brightnessContrastImage(1, $level);
}

return $this;
}
Expand All @@ -274,16 +298,20 @@ public function colorize(int $red, int $green, int $blue): static
$green = Helpers::normalizeColorizeLevel($green);
$blue = Helpers::normalizeColorizeLevel($blue);

$this->image->levelImage(0, $red, $quantumRange['quantumRangeLong'], Imagick::CHANNEL_RED);
$this->image->levelImage(0, $green, $quantumRange['quantumRangeLong'], Imagick::CHANNEL_GREEN);
$this->image->levelImage(0, $blue, $quantumRange['quantumRangeLong'], Imagick::CHANNEL_BLUE);
foreach ($this->image as $image) {
$image->levelImage(0, $red, $quantumRange['quantumRangeLong'], Imagick::CHANNEL_RED);
$image->levelImage(0, $green, $quantumRange['quantumRangeLong'], Imagick::CHANNEL_GREEN);
$image->levelImage(0, $blue, $quantumRange['quantumRangeLong'], Imagick::CHANNEL_BLUE);
}

return $this;
}

public function greyscale(): static
{
$this->image->modulateImage(100, 0, 100);
foreach ($this->image as $image) {
$image->modulateImage(100, 0, 100);
}

return $this;
}
Expand All @@ -300,8 +328,10 @@ public function manualCrop(int $width, int $height, ?int $x = null, ?int $y = nu
->relativePosition($cropped->align(AlignPosition::Center));
}

$this->image->cropImage($cropped->width, $cropped->height, $position->x, $position->y);
$this->image->setImagePage(0, 0, 0, 0);
foreach ($this->image as $image) {
$image->cropImage($cropped->width, $cropped->height, $position->x, $position->y);
$image->setImagePage(0, 0, 0, 0);
}

return $this;
}
Expand Down Expand Up @@ -340,7 +370,9 @@ public function sepia(): static

public function sharpen(float $amount): static
{
$this->image->unsharpMaskImage(1, 1, $amount / 6.25, 0);
foreach ($this->image as $image) {
$image->unsharpMaskImage(1, 1, $amount / 6.25, 0);
}

return $this;
}
Expand Down Expand Up @@ -368,7 +400,9 @@ public function orientation(?Orientation $orientation = null): static
$orientation = $this->getOrientationFromExif($this->exif);
}

$this->image->rotateImage(new ImagickPixel('none'), $orientation->degrees());
foreach ($this->image as $image) {
$image->rotateImage(new ImagickPixel('none'), $orientation->degrees());
}

return $this;
}
Expand All @@ -383,19 +417,20 @@ public function exif(): array

public function flip(FlipDirection $flip): static
{
switch ($flip) {
case FlipDirection::Vertical:
$this->image->flipImage();
break;
case FlipDirection::Horizontal:
$this->image->flopImage();
break;
case FlipDirection::Both:
$this->image->flipImage();
$this->image->flopImage();
break;
foreach ($this->image as $image) {
switch ($flip) {
case FlipDirection::Vertical:
$image->flipImage();
break;
case FlipDirection::Horizontal:
$image->flopImage();
break;
case FlipDirection::Both:
$image->flipImage();
$image->flopImage();
break;
}
}

return $this;
}

Expand All @@ -404,19 +439,22 @@ public function pixelate(int $pixelate = 50): static
$width = $this->getWidth();
$height = $this->getHeight();

$this->image->scaleImage(max(1, (int) ($width / $pixelate)), max(1, (int) ($height / $pixelate)));
$this->image->scaleImage($width, $height);
foreach ($this->image as $image) {
$image->scaleImage(max(1, (int)($width / $pixelate)), max(1, (int)($height / $pixelate)));
$image->scaleImage($width, $height);
}

return $this;
}

public function insert(
ImageDriver|string $otherImage,
AlignPosition $position = AlignPosition::Center,
int $x = 0,
int $y = 0,
int $alpha = 100
): static {
AlignPosition $position = AlignPosition::Center,
int $x = 0,
int $y = 0,
int $alpha = 100
): static
{
$this->ensureNumberBetween($alpha, 0, 100, 'alpha');
if (is_string($otherImage)) {
$otherImage = (new self())->loadFile($otherImage);
Expand All @@ -429,12 +467,14 @@ public function insert(
$watermarkSize = $otherImage->getSize()->align($position);
$target = $imageSize->relativePosition($watermarkSize);

$this->image->compositeImage(
$otherImage->image,
Imagick::COMPOSITE_OVER,
$target->x,
$target->y
);
foreach ($this->image as $image) {
$image->compositeImage(
$otherImage->image,
Imagick::COMPOSITE_OVER,
$target->x,
$target->y
);
}

return $this;
}
Expand All @@ -443,14 +483,16 @@ public function resize(int $width, int $height, array $constraints = []): static
{
$resized = $this->getSize()->resize($width, $height, $constraints);

$this->image->scaleImage($resized->width, $resized->height);
foreach ($this->image as $image) {
$image->scaleImage($resized->width, $resized->height);
}

return $this;
}

public function width(int $width, array $constraints = [Constraint::PreserveAspectRatio]): static
{
$newHeight = (int) round($width / $this->getSize()->aspectRatio());
$newHeight = (int)round($width / $this->getSize()->aspectRatio());

$this->resize($width, $newHeight, $constraints);

Expand All @@ -459,7 +501,7 @@ public function width(int $width, array $constraints = [Constraint::PreserveAspe

public function height(int $height, array $constraints = [Constraint::PreserveAspectRatio]): static
{
$newWidth = (int) round($height * $this->getSize()->aspectRatio());
$newWidth = (int)round($height * $this->getSize()->aspectRatio());

$this->resize($newWidth, $height, $constraints);

Expand All @@ -474,8 +516,8 @@ public function border(int $width, BorderType $type, string $color = '000000'):

$this
->resize(
(int) round($this->getWidth() - ($width * 2)),
(int) round($this->getHeight() - ($width * 2)),
(int)round($this->getWidth() - ($width * 2)),
(int)round($this->getHeight() - ($width * 2)),
[Constraint::PreserveAspectRatio],
)
->resizeCanvas(
Expand All @@ -491,8 +533,8 @@ public function border(int $width, BorderType $type, string $color = '000000'):

if ($type === BorderType::Expand) {
$this->resizeCanvas(
(int) round($width * 2),
(int) round($width * 2),
(int)round($width * 2),
(int)round($width * 2),
AlignPosition::Center,
true,
$color,
Expand All @@ -513,28 +555,34 @@ public function border(int $width, BorderType $type, string $color = '000000'):
$shape->setStrokeWidth($width);

$shape->rectangle(
(int) round($width / 2),
(int) round($width / 2),
(int) round($this->getWidth() - ($width / 2)),
(int) round($this->getHeight() - ($width / 2)),
(int)round($width / 2),
(int)round($width / 2),
(int)round($this->getWidth() - ($width / 2)),
(int)round($this->getHeight() - ($width / 2)),
);

$this->image->drawImage($shape);
foreach ($this->image as $image) {
$image->drawImage($shape);
}

return $this;
}
}

public function quality(int $quality): static
{
$this->image->setCompressionQuality(100 - $quality);
foreach ($this->image as $image) {
$image->setCompressionQuality(100 - $quality);
}

return $this;
}

public function format(string $format): static
{
$this->image->setFormat($format);
foreach ($this->image as $image) {
$image->setFormat($format);
}

return $this;
}
Expand Down
14 changes: 14 additions & 0 deletions tests/ImageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,17 @@
it('will throw an exception when passing an invalid image driver name', function () {
Image::useImageDriver('invalid')->load(getTestJpg());
})->throws(InvalidImageDriver::class);

it('can resize a gif without losing frames when Imagick is used', function () {
$driver = Image::useImageDriver('imagick');
$image = $driver->loadFile(getTestGif());
$targetFile = $this->tempDir->path("{$driver->driverName()}/resize.gif");
$numberOfFrames = count($image->image());
expect($image->getHeight())->toEqual(320);

$image->width(200)->save($targetFile);

$targetImage = $driver->loadFile($targetFile);
expect(count($targetImage->image()))->toBe($numberOfFrames);
expect($targetImage->getWidth())->toEqual(200);
});
6 changes: 6 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ function getTestJpg(): string
return getTestFile('test.jpg');
}


function getTestGif(): string
{
return getTestFile('test.gif');
}

function getTestPhoto(): string
{
return getTestFile('test-photo.jpg');
Expand Down
Binary file added tests/TestSupport/testFiles/test.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.