diff --git a/src/Image/Image.php b/src/Image/Image.php index 146c19a..9b56ca0 100644 --- a/src/Image/Image.php +++ b/src/Image/Image.php @@ -177,7 +177,7 @@ public function crop(int $width, int $height, string $gravity = Image::GRAVITY_C $x = intval($x); $y = intval($y); - if ($this->image->getImageFormat() == 'GIF') { + if ($this->image->getNumberImages() > 1) { $this->image = $this->image->coalesceImages(); foreach ($this->image as $frame) { @@ -188,18 +188,14 @@ public function crop(int $width, int $height, string $gravity = Image::GRAVITY_C $frame->cropImage($width, $height, $x, $y); $frame->thumbnailImage($width, $height); } - } - $this->image->deconstructImages(); - } else { - foreach ($this->image as $frame) { - if ($gravity === self::GRAVITY_CENTER) { - $this->image->cropThumbnailImage($width, $height); - } else { - $this->image->scaleImage($resizeWidth, $resizeHeight, false); - $this->image->cropImage($width, $height, $x, $y); - } + $frame->setImagePage($width, $height, 0, 0); } + } elseif ($gravity === self::GRAVITY_CENTER) { + $this->image->cropThumbnailImage($width, $height); + } else { + $this->image->scaleImage($resizeWidth, $resizeHeight, false); + $this->image->cropImage($width, $height, $x, $y); } $this->height = $height; $this->width = $width; diff --git a/tests/Image/ImageTest.php b/tests/Image/ImageTest.php index 01ef850..741e269 100644 --- a/tests/Image/ImageTest.php +++ b/tests/Image/ImageTest.php @@ -859,4 +859,90 @@ public function test_gif_small_last_frame(): void \unlink($target); } + + /** + * Animated WebP stores delta/partial frames. Cropping without coalesce + * scales those fragments independently and produces ghosting artifacts. + */ + public function test_crop_animated_webp_preserves_frames(): void + { + $source = __DIR__.'/../resources/disk-a/anim-delta.webp'; + $image = new Image(\file_get_contents($source) ?: ''); + $target = __DIR__.'/anim-delta-32x32.webp'; + + $image->crop(32, 32); + $image->save($target, 'webp', 100); + + $this->assertFileExists($target); + $this->assertNotEmpty(\file_get_contents($target)); + + $output = new \Imagick($target); + $this->assertGreaterThan(1, $output->getNumberImages()); + $this->assertTrue(\in_array($output->getImageFormat(), ['PAM', 'WEBP'], true)); + + $coalesced = $output->coalesceImages(); + foreach ($coalesced as $frame) { + $this->assertEquals(32, $frame->getImageWidth()); + $this->assertEquals(32, $frame->getImageHeight()); + } + + // Frame 0 has a red square near the top-left; after a correct resize + // that region must stay red — not blended with later frames. + $coalesced->setFirstIterator(); + $pixel = $coalesced->getImagePixelColor(8, 8)->getColor(); + $this->assertGreaterThan(200, $pixel['r']); + $this->assertLessThan(100, $pixel['g']); + $this->assertLessThan(100, $pixel['b']); + + // Frame 1 replaces that region with green on a correct crop. + $coalesced->nextImage(); + $pixel = $coalesced->getImagePixelColor(8, 8)->getColor(); + $this->assertLessThan(100, $pixel['r']); + $this->assertGreaterThan(180, $pixel['g']); + $this->assertLessThan(150, $pixel['b']); + + \unlink($target); + } + + /** + * Consecutive identical frames are hold/pause frames. Cropping must keep + * total playback delay — deconstructImages() + WebP encode can zero it out. + */ + public function test_crop_animated_webp_preserves_hold_frames(): void + { + $sequence = new \Imagick; + foreach (['#ff0000', '#ff0000', '#0000ff'] as $color) { + $frame = new \Imagick; + $frame->newImage(40, 40, new \ImagickPixel($color)); + $frame->setImageDelay(40); + $frame->setImageDispose(\Imagick::DISPOSE_NONE); + $frame->setImageFormat('gif'); + $sequence->addImage($frame); + } + + $blob = $sequence->getImagesBlob(); + $this->assertNotFalse($blob); + + $image = new Image($blob); + $image->crop(20, 20); + $outputBlob = $image->output('webp', 100); + $this->assertNotFalse($outputBlob); + $this->assertNotNull($outputBlob); + + $output = new \Imagick; + $output->readImageBlob($outputBlob); + + $totalDelay = 0; + foreach ($output as $frame) { + $totalDelay += $frame->getImageDelay(); + } + $this->assertEquals(120, $totalDelay); + $this->assertGreaterThanOrEqual(2, $output->getNumberImages()); + + $coalesced = $output->coalesceImages(); + foreach ($coalesced as $frame) { + $this->assertEquals(20, $frame->getImageWidth()); + $this->assertEquals(20, $frame->getImageHeight()); + } + } } diff --git a/tests/resources/disk-a/anim-delta.webp b/tests/resources/disk-a/anim-delta.webp new file mode 100644 index 0000000..30f5db7 Binary files /dev/null and b/tests/resources/disk-a/anim-delta.webp differ