По работе столкнулся с необходимостью обработки анимированных gif-аватарок. Исходные картинки могут быть любого размера, и их нужно уменьшить до нужного размера с кадрированием до квадрата. Под катом - как мы это решали.
Поскольку проект наш на серверной стороне написан на php - недолго думая мы решили использовать утилиту Imagick. Работаем мы в Ubuntu, посему в 1-2 строчки установить Imagick и php-модуль для него - совсем недолго.
Если взглянуть на документацию Imagick - то видно, что возможностей там полно. В конце поста будет наша готовая функция.
Так как же работать с gif?
Нужно создать два объекта Imagick
<php
# создаём новый пустой объект
$newFileObj = new Imagick();
# оригинальное изображение
$im = new Imagick( $sourceFile );
В данном случае $sourceFile - путь до файла на сервере.
Суть довольно простая - $im - это объект, при помощи которого мы будем работать с gif-изображением. $newFileObj - это объект, который будет хранить данные нового изображения. Простым циклом foreach мы проходим по объекту $im:
<php
foreach ( $im as $newFileObj ) {
$newFileObj->setFormat("gif");
...
}
Имена $newFileObj не случайно совпали. По сути на каждой итерации цикла мы работаем с 1 кадром gif-файла как с отдельным изображением.
Глядя в документацию - не долго искать, как получить информацию о ширине и высоте кадра. После определенных расчётов - сколько и где отрезать от картинки, если она не квадратная, используя метод $newFileObj->cropImage - кадр обрезается. Затем, через метод $newFileObj->setImagePage, мы по сути в новый пустой объект добавляем преобразованный кадр.
Сегодня мы столкнулись с нюансом - не все анимированные gif-изображения корректно обрабатывались. Проблема была в том, что в целях оптимизации фон изображения был в 1 кадре в полном размере, а во всех последующих кадрах были только изменяющиеся фрагменты, которые просто при воспроизведении накладываются на фон. Визуально вы этого не заметите, но по факту каждый такой кадр - это отдельное изображение разного размера. И размер этот был меньше первого кадра, который и определял размер изображения на экране. Поскольку с каждый кадром gif-файла мы работали как с отдельным изображением, изначально мы думали, что все кадры там одного размера. Оказалось, что нет, и это пришлось учитывать.
Для каждого изображения-кадра необходимо было не только расчитать новые размеры, чтобы они были пропорциональны всему изображению, но и расчитать координаты этого изображения, чтобы анимация была на своём месте. Пришлось изрядно поломать мозг (мне, по крайней мере).
Расписывать решение не вижу смысла. Ключевой момент функции:
$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();
# ширина и высота обрезаемой области
# вертикальная картинка
if ( $originalWidth < $originalHeight ) {
$cutedWidth = $originalWidth;
$cutedHeight = $originalWidth;
} else {
#горизонтальная картинка
$cutedWidth = $originalHeight;
$cutedHeight = $originalHeight;
}
$resize_ratio = $cutedHeight / $biggestSideSize ;
$offset_y = $imagePage['y'];
# если размер кадра не совпадает с размером самой картинки
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 до 200 пикселей по ширине и сколько получится по высоте (с соблюдением пропорций, конечно)
$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 );
Отдельно стоит отметить строку:
$im = $im->coalesceImages();
Во время тестирования выяснилось, что при создании на выходе маленьких аватарок - там появлялись артефакты. Эта строчка позволяет избавиться от них. За подсказку спасибо suxxes.
Вот отдельно вся получившаяся функция. Функция создаёт квадрат нужного размера в зависимости от типа изображения. Понимает gif, jpg/jpeg и png. Содержит внутри закомментиованные дебаг-строки (функция dbg), можно раскомментировать и посмотреть, как оно работает.
Функция целиком с кодом и возможностью скачать.
В качестве примера gif-изображения, в котором все кадры - разного размера, предлагаю попробовать этот: