[Из песочницы] Генерируем красивые картинки для социальных сетей

f1094695ef494e78803a01d31ab82dc2.jpg
Код для генерирования именно этого изображения
$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();

Глядя на красивые картинки для соц. сетей, которые в последнее генерируют многие новостные (и не только) сайты — захотелось написать свой генератор.
Примеры картинок
a7f8b24e457e46fc9288b74a908a2eab.jpg

Скрипт работает на 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);

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

Сами параметры оказались такими:

Facebook 1200×630
Twitter 978×511
Google+ 2120×1192 (победитель!)
Вконтакте 537×240
Однокласники 780×585 (уменьшил до 780×385)
При определении социальной сети, скрипт смотрит на User Agent, но тут была одна проблема, не все следуют своей собственной документации.

Так делает Вконтакте. Написано, что обращаясь к сайту он использует vkShare в качестве User Agent. На практике оказалось, что он это делает иногда. Я не знаю с чем это связано, но при попытке расшарить новую ссылку в VK, на страницу заходили несколько раз с совершенно разными браузерами. Иногда там был vkShare.

В итоге, после ряда экспериментов, решил сделать так, что если User Agent не определился, то считаем, что это VK.

В итоге оказался следующий список социальных-роботов:

  • facebookexternalhit
  • vkShare
  • Twitterbot
  • Google
  • 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):

e271bd63b8cf47fbbcb76266159a4441.jpg

c5816f2c682c4e7698d7533006c052fb.jpg

19435808c53e40569a79b4b449917b7f.jpg

755c2f79831c41a4acdcb6490b92858a.jpg

269ef6680e4c4da7bf81cec609833767.jpg

a544c42e676d4f80b5b9f939ad6cec35.jpg

Есть несколько идей, для развития скрипта, например сделать возможность ставить тест относительно друг друга, метод setLogo превратить в addImage и сделать возможным накладывать несколько картинок.

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

Комментарии (1)

  • 31 октября 2016 в 15:16

    0

    Спасибо за статью, было позновательно.

© Habrahabr.ru