Генеруємо красиві картинки для соціальних мереж



Код для генерування саме цього зображення
$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 1200x630
Twitter 978x511
Google+ 2120x1192 (переможець!)
Вконтакте 537x240
Однокласники 780x585 (зменшив до 780x385)
При визначенні соціальної мережі, скрипт дивиться на 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,"ні");
$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,"ні");
$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, в якій вже є багато чого цікавого, про що б хотілося поділитися.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.