Реализация генерации миниатюр (thumbnails) в нескольких размерах

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация генерации миниатюр (thumbnails) в нескольких размерах
Средняя
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Реализация генерации миниатюр (thumbnails) в нескольких размерах

Загружаемые изображения нужно показывать в разных контекстах — карточка товара, превью статьи, аватар, OG-тег. Хранить оригинал и нарезать его на лету при каждом запросе — плохая идея: CPU уходит на рендеринг, латентность растёт. Правильный путь — генерировать набор фиксированных размеров в момент загрузки и отдавать статику.

Подходы к хранению вариантов

Два устойчивых варианта архитектуры:

Eager generation — все размеры создаются сразу при загрузке файла. Подходит, когда набор размеров стабилен и не меняется. Проще логика отдачи, нет промахов кэша.

Lazy generation — размер генерируется по первому запросу, затем кэшируется. Подходит для динамических проектов, где заранее неизвестно, какие размеры понадобятся.

На большинстве проектов достаточно eager-подхода с фиксированным конфигом.

Стек инструментов

Для PHP/Laravel — библиотека intervention/image на базе GD или Imagick. Imagick предпочтительнее: лучше обрабатывает EXIF, поддерживает более широкий спектр форматов, точнее работает с цветовыми профилями.

composer require intervention/image
// config/image.php
return [
    'driver' => 'imagick', // или 'gd'
];

Для Node.js — sharp (libvips под капотом), самый быстрый вариант на рынке:

npm install sharp

Конфигурация размеров

Размеры лучше хранить отдельным конфигом, а не разбрасывать по коду:

// config/thumbnails.php
return [
    'sizes' => [
        'thumb'   => ['width' => 150,  'height' => 150,  'fit' => 'crop'],
        'small'   => ['width' => 320,  'height' => null, 'fit' => 'width'],
        'medium'  => ['width' => 640,  'height' => null, 'fit' => 'width'],
        'large'   => ['width' => 1280, 'height' => null, 'fit' => 'width'],
        'og'      => ['width' => 1200, 'height' => 630,  'fit' => 'crop'],
    ],
];

fit: crop — обрезает с сохранением пропорций по центру. fit: width — масштабирует по ширине, высота пересчитывается.

Сервис генерации

namespace App\Services;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;

class ThumbnailService
{
    public function generateAll(UploadedFile $file, string $basePath): array
    {
        $original = Image::make($file);
        $sizes    = config('thumbnails.sizes');
        $paths    = [];

        // Сохраняем оригинал
        $ext           = $file->getClientOriginalExtension();
        $hash          = sha1_file($file->getRealPath());
        $originalPath  = "{$basePath}/{$hash}.{$ext}";

        Storage::disk('public')->put($originalPath, (string) $original->encode());
        $paths['original'] = $originalPath;

        foreach ($sizes as $name => $params) {
            $img = clone $original;

            if ($params['fit'] === 'crop') {
                $img->fit($params['width'], $params['height'], function ($constraint) {
                    $constraint->upsize(); // не увеличивать маленькие изображения
                });
            } else {
                $img->resize($params['width'], $params['height'], function ($constraint) {
                    $constraint->aspectRatio();
                    $constraint->upsize();
                });
            }

            $sizePath = "{$basePath}/{$hash}_{$name}.webp";
            Storage::disk('public')->put($sizePath, (string) $img->encode('webp', 85));
            $paths[$name] = $sizePath;
        }

        return $paths;
    }
}

Генерируем сразу в WebP — формат поддерживается всеми актуальными браузерами, даёт 25–35% выигрыш в весе по сравнению с JPEG при том же визуальном качестве. Quality 85 — хороший баланс.

Интеграция с моделью

// app/Models/Media.php

protected $casts = [
    'variants' => 'array',
];

public function getUrlAttribute(): string
{
    return Storage::url($this->path);
}

public function variantUrl(string $size): ?string
{
    return isset($this->variants[$size])
        ? Storage::url($this->variants[$size])
        : null;
}

В контроллере при загрузке:

public function store(Request $request, ThumbnailService $thumbnails): JsonResponse
{
    $request->validate(['image' => 'required|image|max:10240']);

    $file   = $request->file('image');
    $paths  = $thumbnails->generateAll($file, 'images/' . date('Y/m'));

    $media = Media::create([
        'path'     => $paths['original'],
        'variants' => $paths,
        'size'     => $file->getSize(),
        'mime'     => $file->getMimeType(),
    ]);

    return response()->json(['media' => $media]);
}

Регенерация при изменении конфига

Если набор размеров меняется, старые файлы нужно перегенерировать. Artisan-команда:

// app/Console/Commands/RegenerateThumbnails.php

public function handle(ThumbnailService $thumbnails): void
{
    Media::whereNotNull('path')
        ->chunkById(100, function ($chunk) use ($thumbnails) {
            foreach ($chunk as $media) {
                // Берём оригинал из storage, генерируем заново
                $stream = Storage::disk('public')->readStream($media->path);
                $temp   = tempnam(sys_get_temp_dir(), 'thumb_');
                file_put_contents($temp, stream_get_contents($stream));

                $uploadedFile = new \Illuminate\Http\UploadedFile(
                    $temp, basename($media->path)
                );

                $dir    = dirname($media->path);
                $paths  = $thumbnails->generateAll($uploadedFile, $dir);
                $media->update(['variants' => $paths]);

                unlink($temp);
                $this->info("Regenerated: {$media->id}");
            }
        });
}

Запускается разово:

php artisan thumbnails:regenerate

Ориентировочные сроки

Настройка библиотеки, конфига размеров, сервиса генерации — 4–6 часов. Интеграция с моделью, загрузчик, команда регенерации — ещё 3–4 часа. Если нужна очередь (генерация в фоне через Job) — плюс 2 часа.