DevGang
Авторизоваться

Как быстро создавать динамические изображения с помощью 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-изображений может быть такой:

  1. Когда ваше приложение React отображает страницу, вы создаете специальную ссылку в метатегах HTML og:image(с идентификатором пользователя, необходимым размером изображения и расширением) на прокси-сервер nginx.
  2. Прокси-сервер проверяет, находится ли изображение уже в хранилище S3 или нет.
  3. Если изображение существует, оно обслуживается; в противном случае делается запрос к сервису 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"]

Источник:

#JavaScript #NodeJS
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу