At work I ran into the need to process animated GIF avatars. The source images can be of any size, and they need to be downsized to a target size with cropping to a square. Below the cut — how we solved it.
Since our project on the server side is written in PHP, without much hesitation we decided to use the Imagick utility. We work on Ubuntu, so installing Imagick and its PHP module takes 1–2 lines and almost no time.
If you take a look at the Imagick documentation — it has plenty of capabilities. The full ready-made function is at the end of the post.
So how do you actually work with a gif?
You need to create two Imagick objects
<php
# create a new empty object
$newFileObj = new Imagick();
# the original image
$im = new Imagick( $sourceFile );
Here $sourceFile is the path to the file on the server.
The idea is fairly simple — $im is the object we’ll use to work with the GIF image. $newFileObj is the object that will store the new image data. With a simple foreach loop we iterate over $im:
<php
foreach ( $im as $newFileObj ) {
$newFileObj->setFormat("gif");
...
}
The matching $newFileObj names aren’t a coincidence. On every iteration we work with one frame of the GIF as a separate image.
Looking through the documentation it’s easy enough to find how to get the frame’s width and height. After some maths — how much and where to crop the image if it isn’t square — we crop the frame using $newFileObj->cropImage. Then via $newFileObj->setImagePage we essentially add the transformed frame into the new empty object.
Today we ran into a subtlety — not all animated GIFs were processed correctly. The problem was that, as an optimisation, the background of the image was in the first frame at full size, while every subsequent frame contained only the changing fragments — which on playback are simply overlaid on the background. You won’t notice this visually, but in fact each such frame is a separate image of a different size. And that size was smaller than the first frame, which was the one defining how the image is displayed on screen. Since we treated each frame of the GIF as a separate image, we initially thought all frames were the same size. They weren’t — and we had to take that into account.
For each frame-image we had to compute not just the new size, so it’s proportional to the whole image, but also the coordinates so that the animation appears in the right place. It bent my brain quite a bit, at the very least.
Going through the entire solution doesn’t feel useful. The key part of the function:
$im = $im->coalesceImages();
foreach ( $im as $newFileObj ) {
$newFileObj->setFormat("gif");
$new_x = 0;
$new_y = 0;
$tmp_new_width = $newWidth;
$tmp_new_height = $newHeight;
$imagePage = $newFileObj->getImagePage();
# width and height of the cropped area
# vertical image
if ( $originalWidth < $originalHeight ) {
$cutedWidth = $originalWidth;
$cutedHeight = $originalWidth;
} else {
# horizontal image
$cutedWidth = $originalHeight;
$cutedHeight = $originalHeight;
}
$resize_ratio = $cutedHeight / $biggestSideSize ;
$offset_y = $imagePage['y'];
# if the frame size doesn’t match the size of the image itself
if ( $newFileObj->getImageWidth() < $newWidth ) {
$tmp_new_width = round( $newFileObj->getImageWidth() / $resize_ratio );
$tmp_new_height = round( $newFileObj->getImageHeight() / $resize_ratio );
$offset_x = $imagePage['x'];
$new_x = round( $offset_x / $resize_ratio );
$new_y = round( $offset_y / $resize_ratio );
} else if ( $newFileObj->getImageHeight() < $newHeight ) {
$tmp_new_width = round( $newFileObj->getImageWidth() / $resize_ratio );
$tmp_new_height = round( $newFileObj->getImageHeight() / $resize_ratio );
$offset_x = $imagePage['x'] - ( $originalWidth - $cutedWidth )/2;
$new_x = round( $offset_x / $resize_ratio );
$new_y = round( $offset_y / $resize_ratio );
}
// Resize down to 200 pixels in width and whatever it works out to in height (preserving aspect ratio, of course)
$newFileObj->thumbnailImage( $tmp_new_width, $tmp_new_height );
if ( $newFileObj->getImageHeight() >= $biggestSideSize || $newFileObj->getImageWidth() >= $biggestSideSize ) {
$newFileObj->cropImage( $biggestSideSize, $biggestSideSize, $src_x, $src_y );
} else {
$newFileObj->cropImage( $biggestSideSize, $biggestSideSize, 0, $src_y );
}
$newFileObj->setImagePage( $newFileObj->getImageWidth(), $newFileObj->getImageHeight(), $new_x, $new_y );
}
$newFileObj->writeImages( $destinationFile, true);
return image_type_to_extension( $info[2], false );
Worth highlighting one line:
$im = $im->coalesceImages();
During testing it turned out that when producing small avatars there were artefacts on the output. This line gets rid of them. Thanks to suxxes for the tip.
Here is the resulting function on its own. The function produces a square of the requested size depending on the image type. It understands gif, jpg/jpeg and png. It contains commented-out debug lines (a dbg function) — feel free to uncomment them to see how it works.
Full function with code and a download link.
As an example of a GIF where every frame is a different size, try this one:
