В наше время многочисленные сайты создают страницы, которыми пользователи хотели бы делиться в разных социальных сетях или мессенджерах. Благодаря тегам Open Graph ссылки могут иметь красочное превью изображение, которое привлекает еще больше внимания, например, с помощью тега og:image
.
Обычно многие веб-сайты не прикладывают особых усилий к предварительному просмотру изображений и просто добавляют одно изображение на большинство страниц. Если изображения нет, парсеры пытаются автоматически найти первое доступное подходящее изображение и использовать его.
Но представьте как бы было здорово иметь персонализированную картинку на странице, например, когда у вас есть профили ваших пользователей, или каких-то событий в которых они участвуют. Такая задача возникла и в нашем проекте, где мы хотели бы генерировать страницы с красочными изображениями которыми пользователи хотели бы делиться.
Например, посмотрите на картинку ниже которую мы генерируем для шаринга в социальных сетях. Тут есть кастомный шрифт, градиент, и даже локализация картинки на разные языки.
В нашем проекте первоначальные версии изображений создавались с помощью PHP, и какое-то время этого было достаточно, поскольку изображения были не очень сложными и содержали только картинку пользователя. Однако как только мы добавили имя пользователя, то уже сразу возникли проблемы с позиционированием текста. Возьмем для примера простой скрипт создания изображения на PHP.
Пример создания изображения на PHP
<?php
$width = 400;
$height = 200;
$image = imagecreatetruecolor($width, $height);
$backgroundColor = imagecolorallocate($image, 255, 255, 255);
imagefill($image, 0, 0, $backgroundColor);
$textColor = imagecolorallocate($image, 0, 0, 0);
$fontFile = 'path/to/your/font.ttf';
$language = isset($_GET['lang']) ? $_GET['lang'] : 'ru';
$text = getLocalizedText($language);
$fontSize = 24;
$textbox = imagettfbbox($fontSize, 0, $fontFile, $text);
$textX = ($width - ($textbox[2] - $textbox[0])) / 2;
$textY = ($height - ($textbox[5] - $textbox[3])) / 2 + ($textbox[5] - $textbox[3]);
imagettftext($image, $fontSize, 0, $textX, $textY, $textColor, $fontFile, $text);
header("Content-Type: image/png");
imagepng($image);
imagedestroy($image);
function getLocalizedText($language) {
switch ($language) {
case 'en':
return 'Hello, PHP!';
case 'fr':
return 'Bonjour, PHP!';
default:
return 'Привет, PHP!';
}
}
Как видно из примера, если текст будет слишком длинный, то может уйти и за рамки изображения. А если сюда добавить градиенты, полупрозрачность, тени или что-то еще интересное, то код усложнится и поддерживать его будет не очень просто.
Все это приводит к тому, что затраты на изменение изображения требуют достаточно большого количества времени разработки.
Решение, о котором я хотел бы рассказать позволяет в разы сократить время разработки и упростить поддержку таких изображений. Более того - оно гибкое и очень легко масштабируется и для других вещей.
Для веб-разработчика HTML - это самый простой и удобный язык разметки. Вместе со стилями CSS вы можете создать гибкий интерфейс, учитывающий расположение любых элементов на странице — изображений, текста, таблиц, списков и т.д.
Давайте посмотрим и на примере ниже. Это простая HTML-страница со множеством стилей CSS, таких как градиент в тексте, тени, ограничение текстовых строк для длинного текста и другое.
Пример HTML кода для страницы профиля
html>
<head>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
background-color: #0093E9;
background-image: linear-gradient(160deg, #0093E9 0%, #80D0C7 100%);
font-family: Helvetica, sans-serif;
padding: 5%;
text-align: center;
}
header {
position: relative;
}
header .emoji {
position: absolute;
top: -10px;
left: 0;
transform: rotate(20deg);
font-size: 3rem;
}
* {
box-sizing: border-box;
}
h1 {
text-transform: uppercase;
font-size: 3rem;
background: -webkit-linear-gradient(45deg, #85FFBD 0%, #FFFB7D 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.wrapper {
display: flex;
padding-top: 2rem;
}
.avatar {
display: flex;
justify-content: center;
align-items: center;
}
.avatar img {
width: 140px;
height: 140px;
border-radius: 100px;
border: 5px solid rgba(255,255,255, 0.5);
box-shadow: 0 0 10px rgba(0,0,0,0.2);
object-fit: cover;
}
.content {
padding: 1rem 2rem;
}
.content .text {
font-size: 1.5rem;
display: -webkit-box;
-webkit-line-clamp: 3; /* number of lines to show */
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
color: rgba(255,255,255, 0.8);
}
</style>
</head>
<body>
<div>
<header>
<h1>Hello, Javascript</h1>
<div class="emoji">?</div>
<header>
<div class="wrapper">
<div class="avatar">
<img src="https://sun9-57.userapi.com/impg/O3egMIWPZjhcKSThZ2hn7ByaQmET8ySOq5e4ww/O_ngP3qqEd8.jpg?size=1178x1789&quality=95&sign=71fcbf49ffff80fad9f0ef39f598cf69&type=album" alt=""/>
</div>
<div class="content">
<div class="text"><strong>Lorem Ipsum</strong> is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</div>
</div>
</div>
</div>
</body>
</html>
Теперь все, что нам нужно, так это запустить веб-сервер, который может обслуживать этот HTML-код, и запустить приложение node.js с библиотекой Puppeteer, которая запускает headless версию Google Chrome, а затем вы можете сделать снимок экрана страницы и получить изображение.
Ниже приведен пример кода, который позволяет получить содержимое страницы с заданной шириной, высотой. Крайне важно обрабатывать исключения и закрывать страницы браузера, если что-то пойдет не так; в противном случае это может привести к утечке памяти.
import puppeteer, { Page } from 'puppeteer-core';
const width = 800;
const height = 600;
const deviceScaleFactor = 1;
const imageType = 'webp';
const imageQuality = 90;
const url = '<Путь к странице на веб сервере>';
// Путь к Chrome или Chromium
// На macOS путь до браузера будет таким "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
// на Linux Для Google Chrome будет "/usr/bin/google-chrome-stable"
const browserPath = '/usr/bin/chromium';
const browser = await puppeteer.launch({
headless: true,
ignoreHTTPSErrors: true,
executablePath: browserPath,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page: Page = await browser.newPage();
await page.setJavaScriptEnabled(false);
await page.setViewport({
width,
height,
deviceScaleFactor,
});
try {
const result = await page.goto(url, {
waitUntil: 'load',
});
if (result?.status() !== 200) {
page.close();
throw new Error(`Incorrect status page ${result?.status()}`);
}
} catch (error) {
page.close();
throw new Error(error as string);
}
const data = await page.screenshot({
type: imageType,
quality: imageQuality,
encoding: 'binary',
});
page.close();
В результате получается вот такое изображение, которое можно сохранить на диск или загрузить в свое S3 хранилище и предоставить его пользователям.
В нашем случае мы создали небольшое приложение node.js с Puppeteer, которое упаковано в контейнер Docker, и запускаем внутри него HTTP-сервер для управления запросами. HTML код мы генерируем с помощью серверного рендера React, в зависимости от переданных данных в запросе. В итоге приложение позволяет нам создавать изображения различного формата (например, png
, jpeg
или webp
) для любой страницы веб-сайта. Также мы отключаем поддержку JS потому что отрисовкой занимается серверный рендер и экономим время загрузки страницы (нам нужны только картинки). С недавних пор мы полностью перешли на генерацию webp
изображений и это позволило нам существенно сократить размер изображений и сделать градиенты (по сравнению с jpeg
) более приятными.
В итоге, полная логика сервиса по генерации OG-изображений получилось такой:
Когда пользователь шарит где-то ссылку, то в
og:image
отдается специальная ссылка (с идентификатором его профиля, необходимым размером и расширением изображения) на прокси-сервер nginx.Прокси-сервер проверяет, находится ли изображение уже в хранилище S3 или нет.
Если изображение существует, оно отдается напрямую; в противном случае делается запрос к сервису node.js, который генерирует изображение, возвращает его и асинхронно загружает на S3.
Несмотря на незначительные технические трудности, новый сервис значительно сократил время на создание и поддержку изображений по сравнению с предыдущим подходом. Такое решение позволяет нам быстро обновлять изображения, просто меняя HTML-код, легко тестировать его в браузере и использовать все возможности CSS.
Само собой такой подход можно использовать и для создания любых других изображений как баннеры или даже, например, PDF файлов с описанием каких-либо товаров. Более того, для Puppeteer есть библиотека puppeteer-screen-recorder благодаря которой можно создавать видео ролики в которой анимация будет создана с помощью CSS. Это тоже может быть полезным потому что, например, iMessage умеет играть небольшие видео которые есть в метатеге og:video
. Возможно в будущем мы увидим такую же возможность, например, и в Telegram.
import puppeteer from 'puppeteer';
import { PuppeteerScreenRecorder } from 'puppeteer-screen-recorder';
function delay(time: number) {
return new Promise(function (resolve) {
setTimeout(resolve, time);
});
}
(async () => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({
width: 640,
height: 480,
});
await page.goto('http://localhost:10001');
const recorder = new PuppeteerScreenRecorder(page, {
fps: 30,
videoCrf: 20,
videoFrame: {
width: 640,
height: 480,
},
});
await recorder.start('./simple.mp4');
await delay(500);
await recorder.stop();
await page.close();
await browser.close();
})();
В итоге получается вот такой ролик
Для удобства вот пример докер-контейнера, который мы используем со всеми необходимыми библиотеками для работы с изображениями.
Пример Docker контейнера
FROM node:18
RUN apt -y update \
&& apt -y install \
git \
openssh-server \
gconf-service \
libasound2 \
libatk1.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgcc1 \
libgconf-2-4 \
libgdk-pixbuf2.0-0 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
ca-certificates \
fonts-liberation \
libappindicator1 \
libnss3 \
lsb-release \
xdg-utils \
wget \
curl \
libnss3-dev \
libgbm-dev \
libu2f-udev \
udev \
libvips \
chromium
# Добавьте нужные вам шрифты
COPY ./fonts /root/.fonts
RUN fc-cache -fv
# Сборка вашего проекта
...
# Запуск приложения
CMD ["node", "app.js"]