Реалізація генерації мініатюр (thumbnails) у кількох розмірах

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація генерації мініатюр (thumbnails) у кількох розмірах
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація генерації мініатюр (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', // or '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 години.