Skip to content

Commit

Permalink
Merge pull request #234 from jhorie/manipulating_animated_gifs
Browse files Browse the repository at this point in the history
Manipulating animated GIF's is now supported using Imagick.
  • Loading branch information
timvandijck committed Feb 16, 2024
2 parents 62a1b5a + a1aaaab commit fdd6bb6
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 61 deletions.
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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit fdd6bb6

Please sign in to comment.