PHP-класс для ресайза изображений

PHP-класс для ресайза изображений

Внимание! Статья устарела. Появилась новая версия продукта. По ней также написана статья, создана документация и написаны демо. Эта статья может использоваться лишь в образовательных целях. Прочитать о новой версии продукта вы можете, перейдя по ссылке.

Задача масштабирования и обрезки изображений в интернете встречается очень часто. Это и социальные сети, и фотохостинги, где из загруженного изображения необходимо создать миниатюру. Однако каждый раз писать php-скрипты для ресайза под какую-то конкретную задачу или заказ – утомительно. Именно поэтому я написал свой класс на php, позволяющий очень удобно изменять размеры изображений и выполнять их обрезку. В этой статье я расскажи о его возможностях и попробую описать все методы масштабирования и кропа (обрезки) изображений. Если вам неинтересно или ненужно знать, как всё устроено и работает, то стоит обратить внимание лишь на использование класса, а сам исходник найдёте в конце статьи. Если же вы более любознательны и хотите полностью понимать, как именно происходит масштабирование, то читаем статью дальше.

Внимание! В статье не будут рассмотрены базовые принципы ООП-программирования. Статья о ресайзе изображений, поэтому, прежде я рекомендую ознакомиться с темой объектно-ориентированного программирования на PHP.

Функциональность

Прежде всего, необходимо определиться с тем, что должен уметь делать наш класс. Я долго изучал различные необходимые методы изменения размеров изображений, вспоминал, какие мне реально пригодились в жизни, и в конце концов мой класс обрёл следующую функциональность:

  • вписывание изображений в указанные рамки;
  • сжатие по указанной ширине;
  • сжатие по указанной высоте;
  • обрезка произвольной области изображения;
  • обрезка квадратной области изображения;
  • “умное” создание миниатюр.

Реализация

Итак, приступим к написанию каркаса самого класса и некоторых вспомогательных функций.

class acResizeImage
{
   private $image; //идентификатор самого изображения
   private $width; //исходная ширина
   private $height; //исходная высота
   private $type; //тип изображения (jpg, png, gif)
 
   function __construct($file)
   {
      if (@!file_exists($file)) exit("File does not exist");
      if(!$this->setType($file)) exit("File is not an image");
      $this->openImage($file);
      $this->setSize();
   }
 
   //функция проверяет, является ли файл изображением и устанавливает его тип
   private function setType($file)
   {
      $mime = mime_content_type($file);
      switch($mime)
      {
         case 'image/jpeg':
            $this->type = "jpg";
            return true;
         case 'image/png':
            $this->type = "png";
            return true;
         case 'image/gif':
            $this->type = "gif";
            return true;
         default:
            return false;
      }
   }
 
   //создаёт в зависимости от типа на основе файла идентификатор изображения
   private function openImage($file)
   {
      switch($this->type)
      {
         case 'jpg':
            $this->image = @imagecreatefromjpeg($file);
            break;
         case 'png':
            $this->image = @imagecreatefrompng($file);
            break;
         case 'gif':
            $this->image = @imagecreatefromgif($file);
            break;
         default:
            exit("File is not an image");
      }
   }
 
   //устанавливает размеры изображения
   private function setSize()
   {
      $this->width = imagesx($this->image);
      $this->height = imagesy($this->image);
   }
}

Конструктор в качестве аргумента принимает адрес файла, после чего проверяет его на существование. Функция setType($file) устанавливает в приватный член класса type тип изображения. Функция, разумеется, приватная, ибо извне класса использоваться не будет. Она узнаёт mime-тип файла и если файл является изображением, в поле type записывает его тип.

После этого в конструкторе вызывается функция openImage($file). Она создаёт на основе файла идентификатор изображения и записывает его в член класса — image. Ну и наконец, setSize() сохраняет размеры изображения в соответствующие поля. Все функции также приватны.

Теперь мы можем создать экземпляр класса acResizeImage:

$img = new acResizeImage('image.jpg');

Естественно, изображение image.jpg должно существовать.

После того, как создан экземпляр класса, мы можем им оперировать (изменять размеры изображения, обрезать его и т. д.). Итак, приступим.

Вписывание в рамки

Предположим, у нас есть фотоальбом и при клике на миниатюру в окошке должно появляться большое фото. Но ведь размеры окошка ограничены. Скажем, мы хотим, чтобы оно было не больше, чем 800×800. Значит, все загружаемые в фотоальбом фотографии должны быть вписаны в эти рамки. Разумеется, фото будет ужиматься пропорционально.
Вписывание в рамки
Для подобного ресайза следует вызвать следующую функцию класса:

$img->resize(800, 800);

Вот сам код функции resize() (добавьте его в класс):

function resize($width = false, $height = false)
{
   /**
   * В зависимости от типа ресайза, запишем в $newSize новые размеры изображения.
   */
   if(is_numeric($width) && is_numeric($height) && $width > 0 && $height > 0)
   {
      $newSize = $this->getSizeByFramework($width, $height);
   }
   else if(is_numeric($width) && $width > 0)
   {
      $newSize = $this->getSizeByWidth($width);
   }
   else if(is_numeric($height) && $height > 0)
   {
      $newSize = $this->getSizeByHeight($height);
   }
   else $newSize = array($this->width, $this->height);
   //создаём новое пустое изображение
   $newImage = imagecreatetruecolor($newSize[0], $newSize[1]);
   imagecopyresampled($newImage, $this->image, 0, 0, 0, 0, $newSize[0], $newSize[1], $this->width, $this->height);
   $this->image = $newImage;
   $this->setSize();
   return $this;
}

Заметьте, оба параметра, принимаемые функцией, необязательны. Позже объясню, зачем это нужно.

В массив $newSize мы записываем новые размеры изображения. В зависимости от параметров, переданных в функцию, новые размеры изображения возвращаются разными функциями:

  • getSizeByFramework($width, $height);
  • getSizeByWidth($width);
  • getSizeByHeight($height).

Если переданы оба параметра (и ширина, и высота), то будущие размеры картинки нам вернёт функция getSizeByFramework(). Это значит, что изображение необходимо вписать в указанные рамки.
Вот код вспомогательной приватной функции:

private function getSizeByFramework($width, $height)
{
   if($this->width <= $width && $this->height <= height) 
	return array($this->width, $this->height);
   if($this->width / $width > $this->height / $height)
   {
      $newSize[0] = $width;
      $newSize[1] = round($this->height * $width / $this->width);
   }
   else
   {
      $newSize[1] = $height;
      $newSize[0] = round($this->width * $height / $this->height);
   }
   return $newSize;
}

Функция возвращает пропорционально изменённые размеры вписанного в рамки изображения. Но вернёмся обратно к функции resize(). После того, как новые размеры картинки получены, на их основе создаётся новое пустое изображение $newImage, после чего вызывается функция:

imagecopyresampled($newImage, $this->image, 0, 0, 0, 0, $newSize[0], $newSize[1], $this->width, $this->height);

Она-то и копирует масштабированное изображение на новое ($newImage).

Внимание, существует ещё одна функция – imagecopyresized(). Она принимает те же параметры, но сжимает изображения без сглаживания. Я не рекомендую её использовать.

Ну и наконец, последние 3 строки перезаписывают исходное изображение на новое, сохраняют новые размеры и (!) возвращают текущий экземпляр класса (об этом мы поговорим позже):

$this->image = $newImage;
$this->setSize();
return $this;

Сжатие изображения по ширине

Вспомните аву «вконтакте»… Вы загружаете фото и оно всегда получается одинаковым по ширине, а высота разная, в зависимости от размеров исходного изображения. Вернёмся опять к функции resize() и посмотрим, что произойдёт, если в функцию передать только ширину:

$img->resize(800);

В данном случае новые размеры изображения нам вернёт функция getSizeByWidth($width), которая считает новые размеры изображения, сжимая картинку пропорционально по ширине.
Сжатие изображения по ширине
Добавьте в класс ещё одну приватную функцию:

private function getSizeByWidth($width)
{
   if($width >= $this->width) return array($this->width, $this->height);
   $newSize[0] = $width;
   $newSize[1] = round($this->height * $width / $this->width);
   return $newSize;
}

Дальше всё происходит так же, как и в случае с вписыванием в рамки — создаётся пустое изображение, на него копируется масштабированный оригинал, фото и размеры перезаписываются на новые, и в программу возвращается текущий объект.

Сжатие изображения по высоте

Задача аналогична предыдущей, но сжимается фото, подгоняясь уже под высоту:

$img->resize(false, 800);

Сжатие изображения по высоте
Размеры высчитываются следующей функцией, которую также не забываем добавить в класс:

private function getSizeByHeight($height)
{
   if($height >= $this->height) return array($this->width, $this->height);
   $newSize[1] = $height;
   $newSize[0] = round($this->width * $height / $this->height);
   return $newSize;
}

Дальнейшие действия в функции resize() аналогичны предыдущим двум случаям.

Crop (произвольная обрезка изображений)

Часто требуется вырезать из исходного изображения какую-либо область. Для этого реализуем функцию crop(). Принцип её работы прекрасно иллюстрирует следующее изображение:
Crop (произвольная обрезка изображений)
X0, Y0 – верхние левые координаты вырезаемой области. Width, height – её, соответственно, ширина и высота.

Используется наш кроп следующим образом:

$img->crop(30, 40, 500, 500);

В этом случае из исходного изображения будет вырезана область 500×500 px., начиная с координаты (30, 40).

Вот сама функция (также добавляем её в класс):

function crop($x0 = 0, $y0 = 0, $w = false, $h = false)
{
   if(!is_numeric($x0) || $x0 < 0 || $x0 >= $this->width) $x0 = 0;
   if(!is_numeric($y0) || $y0 < 0 || $y0 >= $this->height) $y0 = 0;
   if(!is_numeric($w) || $w  $this->width - $x0) $w = $this->width - $x0;
   if(!is_numeric($h) || $h  $this->height - $y0) $h = $this->height - $y0;
   return $this->cropSave($x0, $y0, $w, $h);
}

Как видно, все параметры необязательны. Так, если не передать точки начала обрезки, то это будет точка (0, 0). Передать координаты, находящиеся за пределами фото, также не получится. Если ширина и высота такие, что вырезаемая область выходит за пределы исходного изображения, вырезана будет максимально возможная область. Функция cropSave($x0, $y0, $w, $h) непосредственно вырезает необходимую область и возвращает текущий объект класса. Вот её код:

private function cropSave($x0, $y0, $w, $h)
{
   $newImage = imagecreatetruecolor($w, $h);
   imagecopyresampled($newImage, $this->image, 0, 0, $x0, $y0, $w, $h, $w, $h);
   $this->image = $newImage;
   $this->setSize();
   return $this;
}

Как и в случае с ресайзом, сохраняется новое изображение, размеры и возвращается текущий объект.

Обрезка квадратной области изображения

Вспомним обратно “вконтакте”. Ава у нас может быть прямоугольной, но миниатюры рядом с комментариями, разумеется, квадратные. Причём, какую именно область оригинальной авы отображать на миниатюре, выбираем мы сами. Поэтому, в свой класс я добавил частный случай кропа – квадратный кроп. Его-то сейчас и опишем.

Кстати! В следующих статьях мы обязательно опишем, как с помощью этого класса и JavaScript реализовать такой же принцип обрезки авы, как «вконтакте».
Обрезка квадратной области изображения
В отличие от прошлой функции, в эту можно передать лишь три параметра, которые, как и ранее, все необязательны.

Пример использования:

$img->cropSquare(40, 30, 400);

Первые два параметра – координаты верхнего левого угла вырезаемой области, а третий параметр – сторона самой квадратной области. Как ни странно, но квадратный кроп было значительно тяжелее реализовать. Представьте, что в функцию мы не передали ни одного параметра. Что же тогда делать? Я решил, что в данном случае нужно вырезать максимально большую квадратную область, находящуюся в центре изображения. Примерно вот так:
Обрезка максимальной квадратной области изображения
Если же указаны координаты верхнего левого угла, но не указана сторона квадратной области, то функция вырежет максимальную квадратную область из исходного изображения. Выглядит это так:
Обрезка квадратной области максимального размера
Это нюансы наложили некоторые сложности при написании функции квадратного кропа и теперь она выглядит так:

function cropSquare($x0 = false, $y0 = false, $size = false)
{
   if(!is_numeric($size) || $size width < $this->height)
      {
         $x0 = 0;
         if(!$size || $size > $this->width)
         {
            $size = $this->width;
            $y0 = round(($this->height - $size) / 2);
         }
         else $y0 = 0;
      }
      else
      {
         $y0 = 0;
         if(!$size || $size < $this->height)
         {
            $size = $this->height;
            $x0 = round(($this->width - $size) / 2);
         }
         else $x0 = 0;
      }
   }
   else
   {
      if(!is_numeric($x0) || $x0 = $this->width) $x0 = 0;
      if(!is_numeric($y0) || $y0 = $this->height) $y0 = 0;
      if(!$size || $this->width < $size + $x0) $size = $this->width - $x0;
      if(!$size || $this->height < $size + $y0) $size = $this->height - $y0;
   }
   return $this->cropSave($x0, $y0, $size, $size);
}

Обратите внимание, в конце вызывается уже известная нам функция сохранения обрезаемой области:

return $this->cropSave($x0, $y0, $size, $size);

Вот только ширина и высота вырезаемого изображения равны одной величине – стороне квадрата. Эта функция вернёт текущий экземпляр класса.

«Умное» создание миниатюр

Из заявленного функционала осталось описать лишь создание миниатюр. Попробую рассказать, в чём состоит сама задача. Итак, предположим, мы на своём сайте решили реализовать фотоальбом. Миниатюры в альбомах будут расположены в виде сетки. В чём же проблема? – спросите вы, — Мы ведь умеем уже масштабировать изображения! Тогда смотрите, как у нас будет выглядеть «таблица» миниатюр в альбомах, при классическом ресайзе:
Таблица миниатюр при классическом ресайзе
Не очень, правда? Изображения бывают разные – горизонтальные, вертикальные, квадратные и уж очень вычурные (например, 1000×150). Что только пользователь не загрузит!..

Да, конечно же, можно сжимать все фотографии для миниатюр, скажем, по ширине. В таком случае альбом станет намного миловиднее, но высота превьюшек будет разной. Всё-равно получается не очень. Что же сделаем мы? Мы будем пытаться «заполнить» область одной ячейки таблицы по-максимуму. Да, часть изображения видна не будет, но для миниатюр это не критично. Более того, наша функция будет ещё умнее – если фото слишком сильно вытянуто по одной из сторон, то мы попытаемся его масштабировать так, чтобы в область миниатюры попала наибольшая часть изображения. Рассмотрим всё на примерах:

Сразу договоримся – миниатюры будут отображаться в сетке 3×3, а размеры каждой ячейки соотноситься как 4×3. Пусть для примера размер ячейки, которую будем пытаться максимально заполнить превьюшкой, будет равен 120×90.

Понятно, что фото с пропорциями 4×3 и так заполнит всю ячейку. Этот пример рассматривать не будем. Рассмотрим сперва 2 фото: 16×9 и 9×16. Вот как это будет выглядеть:
Создание миниатюр изображений
Части картинок, которые изображены более тускло, видны не будут – они не поместятся в ячейку, зато вся ячейка будет полностью заполнена и лишь незначительная часть изображения останется “за кадром”. Нас это вполне устраивает.

Но что, если фото сильно вытянуто? Получится, что бОльшая часть изображения не видна, а это нас уже не устраивает, ведь часто не получится даже понять, что изображено на фотографии. Тут придётся идти на “уступки”, заполняя не полностью ячейку, зато сохраняя большую часть изображения на виду. Чуть позже мы поговорим о некотором коэффициенте. Сейчас же скажу, что этот коэффициент указывает, какая доля изображения может быть не видна на миниатюре. Скажем, если он равен двум, то в том случае, когда после сжатия по меньшей стороне, большая сторона более чем в 2 раза превышает соответствующую сторону ячейки, будем немного жертвовать “заполненностью” рамки, уменьшая большую сторону, до 2*сторона_ячейки px.

Покажу на примере:
Хороший и плохой примеры создания миниатюр
В первом случае меньше половины изображения на виду. А во втором случае небольшая область ячейки остаётся незаполненной, но этим мы жертвуем. Стоит заметить, что алгоритм создания миниатюр у меня написан таким образом, что ячейка не может быть заполненной менее чем на половину, кроме, разумеется, тех случаев, когда исходное изображение заведомо меньше ячейки таблицы.

Если картинка ещё более вытянута и одна из её сторон превышает соответствующую сторону рамки более чем в 2*коэффициент раз, то уменьшаем эту сторону ровно в 2 раза, но так, чтобы ячейка не оказалась заполненной менее чем на половину.

С помощью этого механизма таблица с фото, расположенная выше, будет выглядеть следующим образом:
Таблица миниатюр в фотоальбоме
Не правда ли – намного лучше?

Создаём миниатюру следующим образом:

$img->thumbnail(120, 90, 2);

Первые 2 параметра – размеры рамки, которую необходимо заполнить. Третий параметр – тот самый коэффициент (по-умолчанию равен двум).

Добавляем в класс функцию:

function thumbnail($width, $height, $c = 2)
{
   if(!is_numeric($width) || $width width;
   if(!is_numeric($height) || $height height;
   if(!is_numeric($c) || $c getSizeByThumbnail($width, $height, $c);
   $newImage = imagecreatetruecolor($newSize[0], $newSize[1]);
   imagecopyresampled($newImage, $this->image, 0, 0, 0, 0, $newSize[0], $newSize[1], $this->width, $this-height);
   $this->image = $newImage;
   $this->setSize();
   return $this;
}

В данной функции необязательным параметром является лишь коэффициент. Как и при ресайзе, но уже при помощи функции getSizeByThumbnail(), получаем новые размеры изображения. Дальнейшие действия абсолютно аналогичны.

Вот код функции getSizeByThumbnail():

private function getSizeByThumbnail($width, $height, $c)
{
   if($this->width height width, $this->height);
   $realW = $this->width;
   $realH = $this->height;
 
   $rotate = false;
   if($width / $realW  $width)
   {
      if($possH = $width / 2)
            {
               $newSize[1] = $limY;
               $newSize[0] = $realW * $limY / $realH;
            }
            else
            {
               $newSize[0] = $width / 2;
               $newSize[1] = $realH * $newSize[0] / $realW;
            }
         }
         else
         {
            $newSize[0] = $width / 2;
            $newSize[1] = $realH * $newSize[0] / $realW;
         }
      }
   }
   if($rotate)
   {
      $t = $newSize[0];
      $newSize[0] = $newSize[1];
      $newSize[1] = $t;
   }
   return $newSize;
}

Код очень сложно ветвится, так как возможно большое количество нюансов. В первый раз код ветвится, когда мы определяем, больше ли исходная ширина картинки, чем ширина ячейки, в которую её необходимо вписать. Но заметьте, в коде нигде не проверяется, не меньше ли высота изображения, чем высота рамки. Если бы я это написал, код удвоился бы вдвое, поэтому я прибегнул к некоторой хитрости.

Рассмотрим следующие строки кода:

if($width / $realW <= $height / $realH)
{
   $t = $realH;
   $realH = $realW;
   $realW = $t;
   $t = $width;
   $width = $height;
   $height = $t;
   $rotate = true;
}

Этот участок кода проверяет, по какой стороне необходимо производить сжатие.

$width, $height – размеры ячейки.

$realW, $realH – размеры исходного изображения.

$rotate – флаг, говорящий, что мы как бы повернули изображение.

В этом участке кода мы меняем понятия местами. Ширина становится длиной и наоборот. Напомню – это необходимо для того, чтобы размер кода не удвоился.

Также в конце функции есть ещё один блок, который меняет местами значения в возвращаемом массиве сторон:

if($rotate)
{
   $t = $newSize[0];
   $newSize[0] = $newSize[1];
   $newSize[1] = $t;
}

Пожалуй, стоит объяснить пару переменных из кода:

$limX – максимальная ширина нового изображения. Если она превышается, то изображение сжимается по вышеописанным правилам.

$limY – максимальная высота нового изображения.

$possH – высота нового изображения после пропорционального масштабирования по ширине.

Следует заметить, что изображение не обрезается под размеры ячейки. На самом деле все невидимые на странице фотоальбома части картинок остаются в целости и сохранности, просто на странице они ставятся на фон своих ячеек (div`ов) и центрируются. Получается, что области, которые нам угодно не видеть, дабы сохранить ровную сетку фотографий, остаются за кадром. Делается это так:

<div class="”photo”"></div>

И css-свойства:

.photo {
 width: 120px;
 height: 90px;
 background: #e5e5e5 url(image.jpg) no-repeat center center;
}

Вот и всё, все возможности нашего класса мы рассмотрели, однако до сих пор не знаем, как сохранить изображение…

Сохранение изображения

Для сохранения изображения используется функция save(). Она сохраняет изображение, идентификатор которого хранится в члене класса – image.

Вот так можно сохранить получившееся изображение:

$img->save('img/', 'image', 'jpg', false, 100);

Первый параметр указывает каталог, куда следует сохранить изображение. Второй – имя файла. Третий – его тип (jpg, png, gif). Четвёртый параметр отвечает за то, стоит ли перезаписывать файл, если файл с таким именем уже существует. Четвёртый параметр – качество jpg-изображения.

А вот и сам код функции:

function save($path = '', $fileName, $type = false, $rewrite = false, $quality = 100)
{
   if(trim($fileName) == '' || $this->image === false) return false;
   $type = strtolower($type);
   switch($type)
   {
      case false:
         $savePath = $path.trim($fileName).".".$this->type;
         switch($this->type)
         {
            case 'jpg':
               if(!$rewrite && @file_exists($savePath)) return false;
               if(!is_numeric($quality) || $quality < 0 || $quality > 100) $quality = 100;
               imagejpeg($this->image, $savePath, $quality);
               return $savePath;
            case 'png':
               if(!$rewrite && @file_exists($savePath)) return false;
               imagepng($this->image, $savePath);
               return $savePath;
            case 'gif':
               if(!$rewrite && @file_exists($savePath)) return false;
               imagegif($this->image, $savePath);
               return $savePath;
            default:
               return false;
         }
         break;
      case 'jpg':
         $savePath = $path.trim($fileName).".".$type;
         if(!$rewrite && @file_exists($savePath)) return false;
         if(!is_numeric($quality) || $quality < 0 || $quality > 100) $quality = 100;
         imagejpeg($this->image, $savePath, $quality);
         return $savePath;
      case 'png':
         $savePath = $path.trim($fileName).".".$type;
         if(!$rewrite && @file_exists($savePath)) return false;
         imagepng($this->image, $savePath);
         return $savePath;
      case 'gif':
         $savePath = $path.trim($fileName).".".$type;
         if(!$rewrite && @file_exists($savePath)) return false;
         imagegif($this->image, $savePath);
         return $savePath;
      default:
         return false;
   }
}

Если тип изображения передан не был, фото сохранится в исходном формате. Если переданный тип не является одним из трёх основных типов, то вернётся false. В случае успеха вернётся адрес нового изображения.

Использование

Часто может потребоваться сделать несколько изменений с исходным изображением. Это не возбраняется и делается следующим образом:

$img = new acResizeImage('image.jpg'); //создали экземпляр класса
$img->cropSquare(100, 200, 500); //вырезали квадратную область
$img->resize(200, 150); //масштабировали изображение, вписав его в рамки
$path = $img->save('img/', 'image', 'jpg', false, 100); //сохранили

Я же рекомендую делать короче и элегантнее – через цепочку вызовов. Теперь станет понятно, почему при ресайзе, кропе и создании миниатюр мы всегда возвращали текущий объект класса. Вот как это будет выглядеть:

$img = new acResizeImage('image.jpg'); //создали экземпляр класса
$path = $img->cropSquare(100, 200, 1500)->resize(200, 300)->save('img/', 'image', 'jpg', true, 50);

Не правда ли, красивее и удобнее?

Всё, весь функционал описан. На данный момент не могу ручаться, что класс работоспособен в абсолютно всех случаях, но в большинстве их – это так. Я его продолжу дорабатывать и добавлять, возможно, новый функционал. От вас жду отзывов, критики и предложений. Пользуйтесь на здоровье! 🙂

Исходник класса