$generator = new imgGenerator();
$textGenerator=new imgTextGenerator();
$textGeneratorTop=new imgTextGenerator();
$label=$textGeneratorTop
->seTextShadow("#000000", 75, 1, 2, 2)
->setText("Test Site","#ffffff",imgGenerator::position_center_top,"1/12",0 )
->setBackground("#000000",'3%')
->setFont(DR."/upload/fonts/fonts2_7/hinted-PTF55F.ttf");
$text=$textGenerator
->seTextShadow("#000000", 75, 1, 2, 2)
->setText("Морковь как двигатель прогресса человечества","#ffffff",imgGenerator::position_center_center,"1/7",array(0,'5%',0,'5%'))
->setFont(DR."/upload/fonts/fonts2_7/hinted-PTF55F.ttf");
$generator
->addText($text)
->addText($label)
->fromImg($_SERVER["DOCUMENT_ROOT"] . "/upload/dynamic/2016-08/15/carrot-big.jpg")
->resizeFor("autodetect")
->addOverlay(0.5,"#000000")
->show();
Глядя на красивые картинки для соц. сетей, которые в последнее генерируют многие новостные (и не только) сайты — захотелось написать свой генератор.
Скрипт работает на PHP, с использованием модуля Imagick. Писать это на GD2 что-то я не решился.
Алгоритм работы предполагался такой:
- Берем за основу картинку или цвет
- Уменьшаем до нужного размера
- Накладываем сверху полупрозрачный фон
- Устанавливаем логотип
- Добавляем надпись
- Кешируем результат
Помимо всего этого нужна возможность установки отступов, позиционирования, автоматического размера шрифта.
Ниже я буду писать куски кода из готового скрипта, скрипт полностью можно посмотреть на Github.
Создаем основу
Основа может быть либо из цвета, либо из картинки. Тут все просто. Создаем Imagick объект:
Для картинки:
$this->im = new Imagick($this->opts["img"]);
Для цвета:
$this->im = new Imagick();
$this->im->newImage(100,100,$this->opts["color"]);
Уменьшаем
Далее уменьшаем и обрезаем картинку до нужного размера, так как Imgick этого сам не умеет, пишем небольшой метод для этого:
$oldGeometry=$im->getImageGeometry();
$max=max($this->opts["resize_and_crop"]["width"],$this->opts["resize_and_crop"]["height"]);
if($max==$this->opts["resize_and_crop"]["width"]) {
$otn=$oldGeometry["height"]/$oldGeometry["width"];
$width=$max;
$height=$max*$otn;
if($height-$this->opts["resize_and_crop"]["height"] < 0) {
$height=$this->opts["resize_and_crop"]["height"];
$width=$height/$otn;
$x=($width-$this->opts["resize_and_crop"]["width"])/2;
} else {
$x = 0;
}
if($position==imgGenerator::position_center_center) {
$y=($height-$this->opts["resize_and_crop"]["height"])/2;
}
} else {
$otn=$oldGeometry["width"]/$oldGeometry["height"];
$height=$max;
$width=$max*$otn;
if($width-$this->opts["resize_and_crop"]["width"] < 0) {
$width=$this->opts["resize_and_crop"]["width"];
$height=$width/$otn;
$y=($width-$this->opts["resize_and_crop"]["height"])/2;
} else {
$y = 0;
}
if($position==imgGenerator::position_center_center) {
$x=($width-$this->opts["resize_and_crop"]["width"])/2;
}
}
$im->resizeImage($width,$height,Imagick::FILTER_LANCZOS,1,false);
$im->cropimage($this->opts["resize_and_crop"]["width"],$this->opts["resize_and_crop"]["height"],$x,$y);
Но проблема уменьшения была в том, чтоб уменьшить до нужного размера, а в том, чтоб определить, с какой соц. сети был запрос к картинке, после чего выставить нужные параметры уменьшения.
Сами параметры оказались такими:
1200x630 | |
978x511 | |
Google+ | 2120x1192 (победитель!) |
Вконтакте | 537x240 |
Однокласники | 780x585 (уменьшил до 780x385) |
При определении социальной сети, скрипт смотрит на User Agent, но тут была одна проблема, не все следуют своей собственной документации.
Так делает Вконтакте. Написано, что обращаясь к сайту он использует vkShare в качестве User Agent. На практике оказалось, что он это делает иногда. Я не знаю с чем это связано, но при попытке расшарить новую ссылку в VK, на страницу заходили несколько раз с совершенно разными браузерами. Иногда там был vkShare.
В итоге, после ряда экспериментов, решил сделать так, что если User Agent не определился, то считаем, что это VK.
В итоге оказался следующий список социальных-роботов:
- facebookexternalhit
- vkShare
- Twitterbot
- OdklBot
Во время тестирования, в офисе прозвучал от меня довольно смешной вопрос «Кто-нибудь есть в однокласниках?». Никто не признался. Оказалось, что я там сам зарегистрировался когда-то.
Пока писал статью, скрипт обзавелся методом withoutCrop, смысл его в том, что он позволяет уменьшить и спозиционировать картинку, без ее обрезания. Это позволяет улучшить положение, если исходная картинка почти всегда является горизонтальной (например, если это обложка фильма, книги, игры и т.д.).
Накладываем полупрозрачную подложку
$geometry=$this->im->getImageGeometry();
$color=new ImagickPixel($this->opts["overlay"]["color"]);
$overlay->newImage($geometry["width"],$geometry["height"],$color);
$overlay->setImageOpacity($this->opts["overlay"]["opacity"]);
Установка логотипа
После некоторых экспериментов, пришел к выводу, что если логотип будет занимать не более 25% по ширине и высоте от картинки, то смотреться он будет вполне хорошо.
Скрипт позволяет установить лого в любое место на картинке, в том числе и по центру.
Настройки по умолчанию подойдут почти под все случаи, но скрипт позволяет менять размер лого, отступы и позиционирование.
Надпись
Я подозревал, что надпись станет одной из самых больших проблем. Так оно и случилось.
Итак, создаем экземпляр ImagickDraw и устанавливаем у него различные параметры: шрифт, размер шрифта, цвет, стиль, сглаживание:
$draw=new ImagickDraw();
$draw->setFont($this->opts["big_text_font"]);
$draw->setFontSize($fs);
$draw->setFillColor(new ImagickPixel($this->opts["big_text"]["color"]));
$draw->setStrokeAntialias(true);
$draw->setTextAntialias(true);
После этого, до установки выравнивания, разбиваем нашу строку на несколько строк, если она не влезает. Для этого используем queryFontMetrics, которая, о чудо (об этом — ниже), в данном случае работает как надо.
function splitToLines($draw,$text,$maxWidth)
{
$ex=explode(" ",$text);
$checkLine="";
$textImage=new Imagick();
foreach ($ex as $val) {
if($checkLine) {
$checkLine.=" ";
}
$checkLine.=$val;
$metrics=$textImage->queryFontMetrics($draw, $checkLine);
if($metrics["textWidth"]>$maxWidth) {
$checkLine=preg_replace('/s(?=S*$)/',"n",$checkLine);
}
}
return $checkLine;
}
Устанавливаем выравнивание:
$draw->setTextAlignment(Imagick::ALIGN_LEFT);
Используем метод annotation, для отрисовки надписи:
$draw->annotation(0, 0, $this->opts["big_text"]["text"]);
После этого, наш объект ImagickDraw был бы готов и осталось только создать объект Imagick, написать на нем наш текст, при помощи метода drawImage:
$textImage=new Imagick();
$textImage->newImage($textwidth,$textheight,"none");
$textImage->drawImage($draw);
$textwidth $textheight берем из queryFontMetrics, как и при разбивке большой строки. Но не тут-то было. Это все работает более или менее корректно, при выравнивании по левому краю, но при выравнивании нескольких строк по центру или по правому краю, начинало происходить что-то странное. Текст постоянно обрезался то с одной стороны, то с другой и непонятно было каким образом спозиционировать текст так, чтоб он влез в изображение.
В комментариях к методу, на php.net кто-то написал формулу вида:
$baseline = $metrics['boundingBox']['y2'];
$textwidth = $metrics['textWidth'] + 2 * $metrics['boundingBox']['x1'];
$textheight = $metrics['textHeight'] + $metrics['descender'];
Но эта формула тоже не работала.
Честно сказать, как я ни бился, пытаясь найти смысл в массиве от queryFontMetrics в разных вариантах позиционирования текста, разным количеством строк — мне это так и не удалось.
В итоге родился такой метод: высчитываем размеры по подсказке с php.net, но увеличиваем немного ширину и высоту.
$textIm=new Imagick();
$metrics=$textIm->queryFontMetrics($draw, $this->opts["big_text"]["text"]);
$baseline = $metrics['boundingBox']['y2'];
$textwidth = $metrics['textWidth'] + 2 * $metrics['boundingBox']['x1'];
$textheight = $metrics['textHeight'] + $metrics['descender'];
$draw->annotation ($textwidth*1.3, $textheight*1.3, $this->opts["big_text"]["text"]);
Далее создаем картинку в 3 раза больше и рисуем на ней нашу надпись:
$textImage=new Imagick();
$textImage->newImage($textwidth*3,$textheight*3,"none");
$textImage->drawImage($draw);
После чего обрезаем края, при помощи:
$textImage->trimImage(0);
И не забываем после этого использовать setImagePage, это нужно для того, чтоб координаты начала, высота и ширина возвращали новые значения:
$textImage->setImagePage(0, 0, 0, 0);
Тень под текстом
Imagick не умеет ставить тень у текста, но умеет делать тень из картинки. Ок, делаем копию с текстом, превращаем в тень, накладываем одно на другое:
$shadow_layer = clone $textImage;
$shadow_layer->setImageBackgroundColor(new ImagickPixel($this->opts["big_text_shadow"]["color"]));
$shadow_layer->shadowImage($this->opts["big_text_shadow"]["opacity"], $this->opts["big_text_shadow"]["sigma"], $this->opts["big_text_shadow"]["x"], $this->opts["big_text_shadow"]["y"]);
$shadow_layer->compositeImage($textImage, Imagick::COMPOSITE_OVER, 0, 0);
$textImage=clone $shadow_layer;
Кстати, $textImage->trimImage(0); конечно же нужно делать уже после установки тени.
Теперь все работает как надо.
Методы для работы с текстом были выделены в отдельный объект и после этого появилась возможность ставить на картинку сразу несколько надписей, очень удобно, например, для интернет магазина, где есть название товара и цена.
Примеры работы скрипта (размер для VK):
Есть несколько идей, для развития скрипта, например сделать возможность ставить тест относительно друг друга, метод setLogo превратить в addImage и сделать возможным накладывать несколько картинок.
Кстати, если вы дочитали до конца. Немного обо мне: меня зовут Дмитрий и я работаю программистом в небольшой студии. В мои задачи входит в том числе и разработка CMS, в которой уже есть много чего интересного, о чем бы хотелось поделиться.
Автор: Vallefor