Arm1.ru

Ресайз анимированных gif-изображений с помощью Imagick

По работе столкнулся с необходимостью обработки анимированных 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-изображения, в котором все кадры - разного размера, предлагаю попробовать этот:

Ресайз анимированных gif-изображений с помощью Imagick

keyboard_return back