Генерируем красивые картинки для социальных сетей

от автора

Код для генерирования именно этого изображения

$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); 

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

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

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):

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

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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *