$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($_SERVER["DOCUMENT_ROOT"]."/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($_SERVER["DOCUMENT_ROOT"]."/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) |
Так делает Вконтакте. Написано, что обращаясь к сайту он использует 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, в которой уже есть много чего интересного, о чем бы хотелось поделиться.
Комментарии (11)
andriano
31.10.2016 22:19Для генерации такого рода картинок я реализовал свой велосипед, в котором в частности было следующее:
Изменение текста для оптимального отображения в прямоугольном регионе на изображении.
1 — Текст разбивается на слова (однобуквенные слова добавляются к следующему слову);
2 — добавлением по одному слову находится место, когда текст выходит за границы региона;
3 — выходящее по ширине за границы региона слово, переносится на следующую строку;
4 — если перенос приводит к выходу по высоте за границы региона — текст обрезается по предыдущему слову, к тексту добавляется многоточие.
* Ожидается что в тексте не будет настолько длинных слов, которые будут шире чем ширина региона текста.
Для вычисления границ текста использовалась функция QueryMultilineFontMetrics, которой передавались параметры визуализации текста (шрифт, цвет, размер, сам текст)
antirek
01.11.2016 09:54круто,
1. назовите проект как-нибудь абстрактнее, типа imagetexter с каким-нибудь развернутым слоганом: «добавляем текст и лого на КДПВ» (над английским вариантом стоит подумать)
2. функциям setText, setLogo удобнее иметь два параметра: первый собственно что set, а второй необязательный options в виде хеша option1=> '50%', option2 => '#color' и т.д. — это удобно, т.к. мы ставим только то, что действительно важно.Vallefor
01.11.2016 11:12Спасибо!
Всегда были проблемы с придумыванием названий. Хотя последний мой шедевр в названиях — DynamicBlender (объект, который вытаскивает данные из динамического содержимого в CMSке).
Да, в большом количестве параметров у методов тоже вижу проблему. Но массивы не нравится использовать для параметров, так как спустя месяц-два — забываются ключи и приходится переходить внутрь методов, чтоб посмотреть, что и как назвал.
Хочу позже сделать так, чтоб можно было сделать цепочку: ->setText("...")->setTextColor("...")->setTextPosition("...") и т.д. В статье уже немного улучшенная версия, изначально нельзя было ставить несколько надписей и все было в одном объекте.
На setLogo тоже есть планы.antirek
01.11.2016 12:43мне больше всего резануло глаза последовательность «auto»,'5%' в одном методе и '5%', «auto» в другом.
по поводу опций — тут выручит документация, ее можно генерить из описания перед реализацией функции, описание перед функцией помогает держать варианты использования в одном месте. Да, цепочка тоже вариант, тем более что вы уже ее используете.
Вообще вот эти методы — это интерфейс вашего модуля, если вы его потом будете менять, будут испытывать боль все кто начал это использовать. А вы ведь хотите чтобы вашим модулем пользовались?: )Vallefor
01.11.2016 13:05И правда, в «лучших» традициях PHP, создал путаницу с порядком аргументов.
Так как данный модуль уже в нескольких наших проектах используется, то при обновлении, обратную совместимость буду тянуть. К этому в какой-то мере приучила разработка CMS, чтоб обновление не превращалось в ад.
ainu
01.11.2016 12:27Скрипт работает на PHP, с использованием модуля Imagick. Писать это на GD2 что-то я не решился.
Для таких случаев существуют библиотеки-обвязки, например, Imagine.
В первом примере DOCUMENT_ROOT вместо константы DR по особенной причине есть?Vallefor
01.11.2016 12:51Там скорее везде должно быть DOCUMENT_ROOT, просто у меня обычно есть константа и использую ее.
unnforgiven
Спасибо за статью, было позновательно.
phoenixweiss
Тот момент когда ник идеально подходит комментарию.