Как быстро создавать динамические изображения с помощью Node.js и Puppeteer
В наше время многочисленные сайты создают страницы, которыми пользователи делятся в разных социальных сетях или мессенджерах. Благодаря тегам Open Graph ссылки могут иметь изображение предварительного просмотра, которое привлекает еще больше внимания, например с помощью тега og:image
. Но обычно многие веб-сайты не прикладывают особых усилий к предварительному просмотру изображений и просто добавляют одно изображение на большинство страниц. Если изображения нет, парсеры пытаются автоматически найти первое доступное подходящее изображение и использовать его.
Но представьте, как было бы здорово иметь на странице персонализированное изображение, например, когда у вас есть профили ваших пользователей или событий, в которых они участвуют.
Например, взгляните на изображение ниже, которое мы создаем для публикации в социальных сетях. Имеет собственный шрифт, градиент и даже локализацию текста на изображении на разные языки.
В нашем проекте первоначальные версии изображений генерировались с помощью 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);
// Load a custom TrueType font
$fontFile = 'path/to/your/font.ttf';
// Set the text to be displayed (considering localization)
$language = isset($_GET['lang']) ? $_GET['lang'] : 'en';
$text = getLocalizedText($language);
$fontSize = 24;
// Set the position for the text to be displayed
$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 'Hello, PHP!';
}
}
Как видно из примера, если текст слишком длинный или вы хотите изменить его размер, он может выходить за границы изображения. А если вы добавите градиенты, прозрачность, разные шрифты, смайлы, код станет сложным в обслуживании.
Решение, которое я хотел бы представить, позволяет сократить время разработки и упрощает обслуживание таких изображений. Более того, оно гибкое и легко масштабируемое для других целей.
Итак, каждый веб-разработчик знает, что HTML — самый простой и удобный язык разметки. Вместе со стилями CSS вы можете создать гибкий интерфейс, учитывающий расположение любых элементов на странице — изображений, текста, таблиц, списков и т. д.
Давайте посмотрим и на примере ниже. Это простая HTML-страница со множеством стилей CSS, таких как градиент в тексте, тени, ограничение текстовых строк для длинного текста и другие.
<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, которая запускает безголовую версию Google Chrome, а затем вы можете сделать снимок экрана страницы и получить желаемое изображение.
Ниже приведен пример кода, который позволяет захватывать содержимое страницы с заданной шириной, высотой и масштабом. Крайне важно обрабатывать исключения и закрывать страницы браузера, если что-то пойдет не так; в противном случае это может привести к утечке памяти.
const browser = await puppeteer.launch({
headless: true,
ignoreHTTPSErrors: true,
executablePath: browserPath,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
dumpio: true,
});
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 PageNotFoundError(`Incorrect status page ${result.status()}`);
}
} catch (error) {
page.close();
throw new PageFetchError(error as string);
}
const data = await page.screenshot({
type: imageType,
quality: imageQuality,
encoding: 'binary',
});
page.close();
И в результате вы можете получить подобное изображение, которое вы можете загрузить в свое хранилище S3 и предоставить его пользователям. Это действительно просто поддерживать и управлять.
В нашем случае мы создаем небольшое приложение node.js с Puppeteer, которое было упаковано в контейнер Docker, и запускаем внутри него HTTP-сервер для управления внешними запросами. Это приложение позволяет нам создавать изображения различного формата (например, PNG, jpg или webp) для любой страницы веб-сайта.
Итак, полная логика сервиса по генерации OG-изображений может быть такой:
- Когда ваше приложение React отображает страницу, вы создаете специальную ссылку в метатегах HTML
og:image
(с идентификатором пользователя, необходимым размером изображения и расширением) на прокси-сервер nginx. - Прокси-сервер проверяет, находится ли изображение уже в хранилище S3 или нет.
- Если изображение существует, оно обслуживается; в противном случае делается запрос к сервису node.js, который генерирует изображение, обслуживает его и асинхронно загружает на S3.
В ходе разработки мы столкнулись с несколькими незначительными проблемами, такими как:
- Проблемы с Chromium в контейнере Ubuntu Docker были решены путем прямой загрузки Google Chrome.
- Один из наших шрифтов сломал шрифт Apple Emoji. Мы решили заменить его.
Несмотря на незначительные технические трудности, новый сервис node.js значительно сократил время создания и поддержки изображений по сравнению с предыдущим PHP-кодом. Решение позволяет нам быстро обновлять изображения, просто менять HTML-код, легко тестировать его в браузере и использовать всю мощь CSS.
Это отличный способ добиться гибкости и масштабируемости при динамическом создании изображений для любых ваших целей (не только для создания изображений Open Graph).
Вот пример докер-контейнера, который мы используем со всеми необходимыми библиотеками для работы с изображениями.
FROM ubuntu:20.04
RUN ln -s /usr/share/zoneinfo/Etc/UTC /etc/localtime \
&& 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 \
&& (curl -fsSL https://deb.nodesource.com/setup_14.x | bash -) \
&& apt-get install -y nodejs \
&& wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \
&& apt install ./google-chrome-stable_current_amd64.deb \
&& rm -rf ./google-chrome-stable_current_amd64.deb \
&& apt-get clean \
&& rm -rf /var/cache/apt/lists
# Add new fonts
COPY ./fonts /root/.fonts
RUN fc-cache -fv
# Build and run your node.js app
...
# Run node
CMD ["node", "app.js"]