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

Тулкит для ресайза и кропа изображенийДостаточно давно мы написали статью, в которой представили наш класc для ресайза и кропа изображений средствами php. В нём было множество недочётов и ошибок. И вот, наконец, мы выпускаем новую версию нашего продукта, существенно доработав его и исправив. Теперь это уже не одиночный класс, а целый Toolkit.

Фактически весь код был переписан, добавлен некоторый функционал, а также, что самое важное, была реализована «геометрия», позволяющая удобно описывать такие сущности, как размер, точка, прямоугольник. Разумеется, в этот раз мы не станем описывать работу самого кода, ибо это займёт не один десяток тысяч символов статьи и не менее часа вашего времени. Остановимся мы лишь на использовании сего инструментария. Но сперва перечислим более подробно весь список изменений, которым подвергся наш продукт.

Итак, вот он:

  • Существенно переписан весь код;
  • Добавлена «геометрия»;
  • Реализован шаблон проектирования – фабрика (factory), что серьёзно сократило дублирование в коде;
  • Добавлен класс AcColor, позволяющий удобно работать с цветом;
  • Реализованы исключения (exceptions);
  • Исправлено определение типа изображения;
  • Добавлена функция, определяющая, существует ли исходный файл. Работает также и с удалёнными изображениями.
  • Добавлена статическая функция isFileImage, определяющая, является ли файл изображением;
  • Добавлена проверка поддержки библиотекой GD возможности открытия определённых типов файлов;
  • Центральный кроп полностью переписан, с учётом различных принимаемых аргументов (пиксели, проценты, пропорции);
  • Добавлена возможность установки цвета фона на прозрачные области изображений (по-умолчанию — белый);
  • Добавлена возможность размещения логотипа на исходном изображении;
  • Добавлена статическая функция setTransparency, включающая, либо отключающая поддержку прозрачности изображений (по-умолчанию поддержка прозрачности включена);
  • Добавлена статическая функция setRewrite, устанавливающая права на перезапись файлов при конфликте имён сохраняемых изображений (по-умолчанию перезапись отключена);
  • Добавлена статическая функция setQuality, устанавливающая качество сохранения JPG-изображений (по умолчанию — 85);
  • Функция save теперь сохраняет изображение в исходном формате;
  • Реализованы функции saveAsJPG, saveAsPNG и saveAsGIF, позволяющие сохранять изображения в соответствующих форматах;

Начнём, пожалуй, с «геометрии», ибо она в наибольшей мере изменила код класса. Мы реализовали три вспомогательных класса: Point, Rectangle и Size, которые, соответственно, описывают точку, прямоугольник и размер. При помощи этих классов удалось избавиться от столь частого дублирования в коде. В подробности работы и функционала «геометрии» мы вдаваться не станем, так как это займёт много времени. Да и необходимости это знать нет, ибо пользователь класса может обойтись без их использования (а оперировать ими будет лишь сам класс AcImage). Впрочем, возможность их использования мы опишем ниже, рассматривая функционал.

Немаловажным новшеством является появление исключений (exceptions). Теперь мы можем заключить код в блоки try{…} и catch(){…} и обработать различные непредвиденные ситуации, собственноручно определив механизм исключительной ситуации. Например, если открываемый файл не обнаружен, мы хотим вывести некое окошко, оповещающее об этом. Сделать это можно будет так:

try
{
   //создаём экземпляр класса
}
catch (FileNotFoundException $ex)
{
   //выводим необходимое оформление окошка
   $ex->getMessage(); //Выведет 'File not found'
}

Также был реализован вспомогательный класс AcColor, необходимый для удобной работы с цветом. Его мы также не станем описывать, так как дёргается он в основном не нами, а классом AcImage.

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

В использовании нашего тулкита осталось мало чего похожего на первую версию. Первые же изменения коснулись создания экземпляра класса. Теперь у нас нет необходимости самим создавать его, это сделает фабрика…

Создание экземпляра класса

В старой версии класса код зачастую ветвился и дублировался. В основном, из-за того, что функционал при работе с различными типами изображения был как похож, так немного и отличался друг от друга. Поэтому, нами было решено вынести код, отвечающий за работу с конкретным типом изображений (jpg, png, gif) в отдельные классы-потомки, наследующие родительский класс AcImage. Это классы AcImageJPG, AcImagePNG, AcImageGIF. Создание же экземпляра класса будет производиться следующим образом:

$img = AcImage::createImage('image.jpg');

Функция же createImage сама решит, экземпляр какого из трёх классов создать, после чего вернёт его нам. Так мы реализовали фабрику.

uml фабрики

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

$img = AcImage::createImage('http://site.com/image.jpg');

Реализована и корректная проверка на то, является ли файл изображением. В противном случае будет брошено исключение.

Про наследование в php можно прочитать по ссылке.

Бросаемые методом исключения:

  • GDnotInstalledException– библиотека GD не подключена;
  • InvalidFileException – файл не является изображением;
  • FileNotFoundException – файл не найден.

Ресайз изображений

В плане использования метода ресайза, класс также изменился. Теперь есть два варианта его использования: использование двух параметров (высоты и ширины области, в которую необходимо пропорционально вписать наше изображение) типа int или одного – экземпляра класса Size.

Вот оба примера использования:

$img = AcImage::createImage('image.jpg');
//первый вариант
$img->resize(640, 480);
//второй вариант
$size = new Size(640, 480); //можно провести некоторые манипуляции с этим экземпляром
$img->resize($size);

Результат выполнения будет одним и тем же – изображение пропорционально впишется в рамки (640×480). Примерно так:

Иллюстрация обычного ресайза
$img->resize(640, 480)

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

$img->resizeByWidth(640); //ужать по ширине до 640px

Сжатие по ширине
$img->resizeByWidth(640)

$img->resizeByHeight(480); //ужать по высоте до 480px

Сжатие по высоте
$img->resizeByHeight(480)

Бросаемые методом исключения:

  • IllegalArgumentException – переданы некорректные аргументы.

Кроп

Как и ресайз, кроп можно использовать двумя способами – используя координаты вырезаемой области или же используя экземпляр класса Rectangle (прямоугольник). В первом случае мы передаём в метод crop координаты верхнего левого угла, а также ширину и высоту области, которую желаем вырезать из исходного изображения. Делается это следующим образом:

$img = AcImage::createImage('image.jpg');
$img->crop(100, 200, 640, 480);

Как и с ресайзом, можно поступить следующим образом:

$rect = new Rectangle(100, 200, 640, 480);
$img->crop($rect);

Кроп
$img->crop(100, 200, 640, 480)

В этой версии тулкита, появилась возможность передавать отрицательные координаты, например, такие:

(-100, 30, 500, 600).

Или такие:

(600, 300, -500, -400)

В обоих случаях будет вырезана корректная область, без пустот.

Кроп с отрицательными параметрами
$img->crop(600, 300, -500, -400)

Бросаемые методом исключения:

  • IllegalArgumentException – переданы некорректные аргументы.

Квадратный кроп

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

Использовать этот кроп можно тремя способами, как передавая параметры верхнего левого угла и ширину вырезаемого квадрата, так и передавая экземпляры классов «геометрии». Делается это следующими способами:

$img = AcImage::createImage('image.jpg');
$img->cropSquere(100, 200, 400);

Первые два аргумента – координаты верхнего левого угла вырезаемого квадрата (они должны быть исключительно положительными). Третий аргумент – собственно, сама сторона вырезаемого квадрата (она же может быть отрицательной, лишь бы полученная область не выходила за пределы исходного изображения).

Во втором случае используется экземпляр класса Point (точка, она же – верхний левый угол) и ширина, значения int:

$img = AcImage::createImage('image.jpg');
$point = new Point(100, 200);
$img->cropSquere($point, 400);

Третий же способ предусматривает лишь один аргумент – прямоугольник (экземпляр класса Rectangle). Но есть один небольшой нюанс – он обязательно должен быть квадратом, иначе будет брошено исключение.

$img = AcImage::createImage('image.jpg');
$rect = new Rectangle(100, 200, 400, 400);
$img->cropSquere($rect);

Выглядеть это будет так:

Квадратный кроп
$img->cropSquere(100, 200, 400)

Кроп с отрицательной стороной квадрата хорошо иллюстрирует следующая картинка:

Квадратный кроп с отрицательной стороной квадрата
$img->cropSquere(500, 400, -300)

Если же вырезаемая область выходит за пределы исходного изображения, будет брошено исключение:

Квадратный кроп, выходящий за пределы изображения
$img->cropSquere(500, 400, 600)

Бросаемые методом исключения:

  • IllegalArgumentException – переданы некорректные аргументы.

Центральный кроп

Центральный кроп позволяет вырезать из исходного изображения некоторую область. Но, в отличие от обыкновенного кропа, описанного ранее, вырезается область из центра картинки. Данная возможность может быть необходима, например, в случае, когда необходимо сделать красивые превью одинакового размера. В этом случае вы вырезаете максимально большую область из центра в пропорциях, например, 4×3.

Способов использования центрального кропа также несколько. Можно вырезать центральную область, строго указав её размеры в пикселях, можно указать ширину и высоту в процентах от оригинального изображения, а можно просто указать соотношение ширины и высоты вырезаемой области.

Остановимся на каждом моменте отдельно.

Самый простой способ использования этого вида кропа – фиксированные размеры вырезаемой области, в пикселях. Делается это одним из следующих способов:

$img = AcImage::createImage('image.jpg');
$img->cropCenter(640, 480); //первый способ
$img->cropCenter('640px', '480px'); //второй способ

Работают оба варианта абсолютно одинаково – из центра исходной картинки вырежется область, размером 640×480. Выглядит это так:

Центральный кроп с фиксированными параметрами
$img->cropCenter(‘640px’, ‘480px’)

Если по какой-либо стороне будет указано значение, превышающее исходный размер картинки, будет вырезана максимально возможная область.

Центральный кроп с параметрами, выходящими за пределы изображения
$img->cropCenter(‘640px’, ‘300px’)

Кроме указания области в пикселях, можно использовать и проценты. Например, так:

$img = AcImage::createImage('image.jpg');
$img->cropCenter('50%', '25%');

В этом случае по ширине изображения из центра будет вырезана половина, а по высоте, четверть.

Центральный кроп с процентами
$img->cropCenter(‘50%’, ‘25%’)

Если значения будут больше 100%, то, как и в прошлом примере, ничего страшного не произойдёт – будет вырезана максимально возможная область.

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

Сетка превью в фотоальбоме

Некрасиво, не правда ли? В таком случае было бы удобно, чтобы отношение сторон всех превью были, скажем, 4×3. То есть, для создания таких миниатюр, нам необходимо вырезать из исходного изображения максимально возможную область с отношением сторон 4×3. Визуально это выглядит так:

Центральный кроп с пропорциями
$img->cropCenter(‘4pr’, ‘3pr’)

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

$img = AcImage::createImage('image.jpg');
$img->cropCenter('4pr', '3pr');

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

Сетка превью в фотоальбоме после кропа

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

$img = AcImage::createImage('image.jpg');
$img->cropCenter('500px', '50%'); //первый вариант
$img->cropCenter('50%', '500px'); //второй вариант

В первом варианте вырезанная область будет иметь ширину 500px, а высота будет составлять половину от высоты исходного изображения. Во втором варианте – наоборот.

Смешанный центральный кроп
$img->cropCenter(‘500px’, ‘50%’)

Следует заметить, что смешивать можно только проценты и пиксели. Ни в коем случае с пропорциями сочетать ничего другого нельзя. Думаю, можете догадаться и сами, почему.

Весьма полезно будет взять на вооружение следующую практику. Предположим, что мы хотим вырезать область, которая по высоте занимает всё изображение, а по ширине – какой-то фиксированный размер. В этом случае также удобно использовать смешанные аргументы, где высота будет равна 100%.

$img = AcImage::createImage('image.jpg');
$img->cropCenter('500px', '100%');

Обрезка по ширине
$img->cropCenter(‘500px’, ‘100%’)

Бросаемые методом исключения:

  • IllegalArgumentException – переданы некорректные аргументы.

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

Чуть ранее, описывая принцип работы центрального кропа, мы рассказали, как создать красивые и одинаковые превью для фотоальбома. Но в том случае все миниатюры физически обрезались, да и не всегда могли получиться содержательными: представьте, что мы увидим на картинке, размером 1000×200, из которой вырезали часть 200×200). Примерно, следующее:

Неудачное использование центрального кропа

Намного лучше было бы, чтобы большая часть изображения оказалась на миниатюре. Именно для этого мы реализовали так называемое умное создание миниатюр. В этом случае изображения физически обрезаться не будут, но будут ужиматься в особые рамки. Некоторая работа тут ляжет на верстальщика, ибо ему придётся спрятать ненужные области, центрировав на фоне дива картинку. Об этом расскажем ниже.

Итак, сперва использование:

$img = AcImage::createImage('image.jpg');
$img->thumbnail(200, 150);

В данном случае изображение так впишется в рамки, что будет занимать всю указанную область, не оставляя пустых мест. Однако, изображение наверняка не будет полностью помещаться в них, а слегка будет выходить за пределы. Верстальщику необходимо их спрятать за пределы дива. В данном случае гарантируется полная сохранность изображения – ничего от него не будет отрезано.

Умное создание миниатюр
$img->thumbnail (200, 150)

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

Примеры плохого и хорошего создания миниатюр

Для определения этого самого предела, когда необходимо жертвовать заполненностью области превью, используется специальный коэффициент, равный двум. То есть, если часть исходного изображения более чем на половину (1/2) оказывается вне области миниатюры, то картинка ужимается так, чтобы большая её часть всё же оказалась внутри. При этом, разумеется, появляются пустые области. Этот коэффициент можно поменять, передав в функцию thumbnail третий (необязательный) параметр. Стоит заметить, что практической надобности делать это в большинстве случаев нет, ибо коэффициент 2 наиболее оптимален. Если всё же вам по какой-то причине захотелось его изменить, делается это следующим образом:

$img = AcImage::createImage('image.jpg');
$img->thumbnail(200, 150, 3); //коэффициент превышения равен трём

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

Превью в фотоальбоме

Второй вариант:

Превью в фотоальбоме с применением умного ресайза

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

<div style="background: url(images/img.jpg) 
center  #999 no-repeat;  
width: 200px; height: 150px"></div>

Этот код помещает на фон div’а миниатюру, пряча не влезающую часть. В случае, если высота или ширина миниатюры оказалась меньше высоты или ширины div’а, пустые поля будут серого цвета (#999). Стоит заметить, что размеры div’а должны совпадать с размерами, которые были переданы методу thumbnail.

Бросаемые методом исключения:

  • IllegalArgumentException – переданы некорректные аргументы.

Размещение логотипа

Лого

В этой версии продукта мы также реализовали и отрисовку логотипа на исходном изображении. Он может быть размещён в одном из четырёх углов картинки, причём, по умолчанию, его размер (ширина либо высота) не сможет превышать 0.1 (10%) от размера (высоты либо ширины) исходного изображения. Отступы же логотипа от края составят 0.02 (2%). Эти значения, разумеется, можно изменить, но об этом позже. Сперва расскажем, как же поместить логотип на изображение.
Опять же, способ не один. Вот первый вариант:

$img = AcImage::createImage('image.jpg');
$img->drawLogo('logo.png', AcImage::BOTTOM_RIGHT);

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

Если же изображение логотипа следует разместить в другом углу, стоит передавать одно из следующих значений: TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT.

Второй способ размещения лого состоит в том, что в метод drawLogo необходимо передавать не ссылку на файл с логотипом, а экземпляр класса изображения-лого.

$img = AcImage::createImage('image.jpg');
$logo = AcImage::createImage('logo.png');
$img->drawLogo($logo, AcImage::BOTTOM_RIGHT);

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

Варианты размещения лого

Естественно, заботиться о размерах логотипа вам не следует – его размер будет пропорционально уменьшен.
Напоследок лишь стоит упомянуть о некоторых сеттерах. Так, вы можете изменить относительный размер лого. Сейчас по любой из сторон он не сможет быть больше 10% от стороны исходного изображения.

//теперь по высоте и ширине лого не будет больше 15% от исходного изображения
AcImage::setMaxProportionLogo(0.15);

Разумеется, значение, передаваемое в функцию setMaxProportionLogo должно быть больше нуля и не более единицы.

Также можно изменить отступ логотипа от края изображения. По умолчанию он равен двум процентам.

//изменили отступ от края на 5%
AcImage::setPaddingProportionLogo(0.05);

Следует, пожалуй, заметить, что на наш взгляд эти значения наиболее оптимальны и стоит подумать, прежде чем их менять.

Бросаемые методом исключения:

  • IllegalArgumentException – переданы некорректные аргументы.
  • GDnotInstalledException– библиотека GD не подключена;
  • InvalidFileException – файл не является изображением;
  • FileNotFoundException – файл не найден.

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

Принцип сохранения изображений также претерпел значительные изменения. Теперь для этого существуют несколько функций: save, saveAsJPG, saveAsPNG и saveAsGIF. Первая сохраняет изображение в исходном формате, остальные — думаю, понятно.

То есть, если исходным являлось PNG-изображение, то следующим образом мы его и сохраним как PNG:

$img = AcImage::createImage('image.png');
//производим какие-либо действия с изображением
$img->save('img/image.png');

Функция save, как и три другие функции в качестве аргумента принимает адрес, по которому мы желаем сохранить картинку. Функции saveAsJPG, saveAsPNG и saveAsGIF работают аналогично, но сохраняют изображения в соответствующих форматах.

$img = AcImage::createImage('image.png');
//производим какие-либо действия с изображением
$img-> saveAsJPG('img/image.jpg');

Цепочки вызовов

Очень хорошей возможностью, как и в прошлой версии класса, являются цепочки вызовов, позволяющие удобно выполнить с изображением сразу несколько различных действий. Делается это очень просто. Например, так:

$img = AcImage:: createImage('image.jpg');
$img
   ->resize(640, 480)
   ->crop(100, 100, 400, 300)
   ->drawLogo('logo,jpg', AcImage::BOTTOM_RIGHT)
   ->save('img/image.jpg');

Таким образом, сначала изображение будет вписано в рамки 640×480, затем из него будет вырезана область (100, 100, 400, 300), на которой, впоследствии, будет нарисован логотип из файла logo.jpg. В конечном итоге, изображение будет сохранено по адресу img/image.jpg. Возможно это благодаря тому, что все функции возвращают экземпляр текущего класса — $this.

Некоторые полезные статические функции

Кроме вышеописанного функционала мы реализовали множество геттеров и сеттеров. Описывать их всех мы не станем, а остановимся лишь на основных.

isFileExists

C помощью этой статической функции мы можем ещё до создания экземпляра класса проверить, существует ли файл. В случае если файл существует, вернётся true, и false – в противном случае. Использовать это мы можем, например, так:

if(AcImage::isFileExists('image.jpg'))
   $image = AcImage::createImage('image.jpg');
else echo "Файл не существует";

isFileImage

Это также статическая функция. Она проверяет, является ли файл изображением. Например, следующим образом:

if(AcImage::isFileImage('image.jpg'))
   $image = AcImage::createImage('image.jpg');
else echo "Файл не является изображением";

setRewrite

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

AcImage::setRewrite(true); //разрешить перезапись при конфликте имён
AcImage::setRewrite(false); //запретить перезапись при конфликте имён

По умолчанию, перезапись при конфликте имён запрещена.

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

setQuality

Здесь всё просто. Эта статическая функция устанавливает качество сохраняемых изображений. По умолчанию установлено значение, равное 85. Диапазон возможных значений – (0, 100].

AcImage::setQuality(100);

КачествоsetTransparency

Функция указывает, стоит ли сохранять в исходных изображениях прозрачность. Возможные принимаемые значения – true (сохранять прозрачность) и false (не сохранять прозрачность).

Если значение установлено в true, то изображения с прозрачностью такими и останутся, если, разумеется, не будут сохранены в jpg. В противном случае, области с прозрачностью будут «залиты» определённым цветом (об этом расскажем далее).

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

AcImage::setTransparency(true); //поддержка прозрачности включена
AcImage::setTransparency(false); //поддержка прозрачности отключена

Прозрачность

setBackgroundColor

А теперь представьте, что мы загрузили некое PNG-изображение с прозрачными областями. Так что же с ним будет после сохранения в JPG? Прозрачные области будут заполнены некоторым цветом. По умолчанию – белый цвет. Однако его можно изменить. Делается это при помощи функции setBackgroundColor. Установить цвет можно двумя способами — передав RGB-составляющие или же передав экземпляр класса AcColor. Вот примеры использования обоих вариантов:

//первый вариант
AcImage::setBackgroundColor(100, 100, 100);
//второй вариант
$color = new AcColor(100, 100, 100);
AcImage::setBackgroundColor($color);

Разумеется, все RGB-составляющие должны лежать в пределах от 0 до 255. В случае передачи некорректных аргументов будет брошено исключение IllegalArgumentException.

Мы описали не все сеттеры. Если же вам они будут интересны, вы сможете их найти в документации к тулкиту.

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

Обновлено 12 марта 2013

Были исправлены ошибки, связанные с определение поддержки библиотеки GD. Были изменены методы AcImage::isSupportedGD и AcImage::getGDinfo. Исправлена ошибка в методе AcImageJPG::isSupport(), который некорректно работал для php версии ниже 5.3. Добавлен метод AcImage::getShortPHPVersion().