Jak generować miniaturki zdjęć w PHP


Ilość ocen: 0 Średnia ocena: -/5

Miniaturki zdjęć to małe wielkie rzeczy – decydują o szybkości strony, wygodzie użytkownika i wyniku w wyszukiwarce. Pokażę Ci, jak w praktyce generować miniaturki w PHP, tak by były lekkie, ostre i zawsze idealnie dopasowane. Dorzucę też garść naszych patentów z RemnetCMS, które pomagają robić to szybko i bezpiecznie.

Dlaczego miniaturki mają znaczenie

Miniaturki (thumbnails) to nie tylko pomniejszone obrazy. To przemyślana wersja grafiki, która zdejmuje ciężar z serwera i przeglądarki, poprawia Core Web Vitals i wspiera SEO. Kiedy zdjęcia ładują się błyskawicznie, użytkownicy zostają dłużej, klikają częściej, a strona działa płynnie nawet na wolnym łączu. W Remnet tworzymy miniaturki tak, aby miały sens biznesowy: szybkie listy produktów, blogi bez „skaczących” elementów i galerie, które nie zabijają transferu danych.

Dwie drogi w PHP: GD czy Imagick?

W świecie PHP miniaturki najczęściej generuje się za pomocą biblioteki GD albo Imagick. Obie dadzą radę, ale różnią się możliwościami i wydajnością. W praktyce wybór zależy od serwera, dostępnych formatów i skali projektu.

CechaGDImagick
DostępnośćDomyślnie dostępna w wielu hostingachWymaga zainstalowanego ImageMagick
Jakość i ostrośćDobra przy właściwym skalowaniuBardzo dobra, świetne algorytmy
WydajnośćLekka, ale mniej zaawansowanaSzybka i zoptymalizowana dla większych plików
Wsparcie formatówJPEG, PNG, GIF, WebP (zależnie od wersji)JPEG, PNG, GIF, WebP, AVIF (jeśli ImageMagick je wspiera)
Dodatkowe efektyPodstawoweBogate: rozmycia, ostrość, profil kolorów

Proporcje, kadrowanie i tryby skalowania

Kluczem do dobrych miniaturek jest przewidywalny kadr. Najczęściej używamy dwóch podejść:

  • cover (wypełnienie) – miniatura ma dokładne wymiary, a obraz jest przycięty, by wypełnić całą ramkę; idealne dla listingów i kart produktu.
  • contain (dopasowanie) – obraz mieści się w ramce bez przycinania, mogą zostać marginesy; dobre dla galerii i obiektów o różnych proporcjach.

Jeśli chcesz pilnować „ważnego” miejsca na zdjęciu (np. twarz, produkt), rozważ mechanizm punktu skupienia – przechowywany w metadanych i używany podczas kadrowania. W RemnetCMS często stosujemy domyślny crop centralny plus opcję ręcznej korekty, by panować nad estetyką siatek zdjęć.

Bezpieczne i szybkie przetwarzanie – plan działania

  1. Weryfikacja pliku: przed obróbką sprawdź MIME (getimagesize) i rozszerzenie. Nie ufaj danym od klienta.
  2. Limit pamięci: duże zdjęcia potrafią wywołać błąd pamięci. Oszacuj RAM (szerokość × wysokość × 4 bajty) i w razie potrzeby odrzuć lub przeskaluj wstępnie.
  3. Orientacja EXIF: telefony zapisują rotację w EXIF – trzeba ją uwzględnić, w przeciwnym razie miniatury będą „na boku”.
  4. Wybór formatu wyjściowego: preferuj WebP (a jeśli środowisko wspiera – AVIF) ze względu na świetny stosunek jakości do wagi.
  5. Nazewnictwo: zapisuj miniatury z parametrami w nazwie, np. produkt-800x600-cover.webp, aby łatwo je cache’ować.
  6. Cache i nagłówki: długie max-age i immutable dla niezmiennych plików. Przyspiesza to kolejne odwiedziny i score w Lighthouse.
  7. Zapisy atomowe: generuj do pliku tymczasowego i wykonuj rename, by uniknąć wyścigu zapisu przy równoczesnych żądaniach.

Przykład z biblioteką GD

Prosty, praktyczny przykład generowania miniatury w trybie cover i contain. Obsługuje orientację EXIF i zapis do WebP/JPEG/PNG.

<?php
function createThumbnailGD($srcPath, $dstPath, $targetW, $targetH, $mode = 'cover', $format = 'webp') {
    if (!file_exists($srcPath)) {
        throw new RuntimeException('Brak pliku źródłowego');
    }
    [$w, $h, $type] = getimagesize($srcPath);
    $mime = image_type_to_mime_type($type);

    switch ($mime) {
        case 'image/jpeg': $src = imagecreatefromjpeg($srcPath); break;
        case 'image/png':  $src = imagecreatefrompng($srcPath);  break;
        case 'image/gif':  $src = imagecreatefromgif($srcPath);  break;
        case 'image/webp': $src = function_exists('imagecreatefromwebp') ? imagecreatefromwebp($srcPath) : null; break;
        default: $src = null; break;
    }
    if (!$src) {
        throw new RuntimeException('Nieobsługiwany format obrazu');
    }

    // Korekta orientacji EXIF dla JPEG
    if ($mime === 'image/jpeg' && function_exists('exif_read_data')) {
        $exif = @exif_read_data($srcPath);
        if (!empty($exif['Orientation'])) {
            switch ($exif['Orientation']) {
                case 3: $src = imagerotate($src, 180, 0); break;
                case 6: $src = imagerotate($src, -90, 0); [$w,$h] = [$h,$w]; break;
                case 8: $src = imagerotate($src, 90, 0);  [$w,$h] = [$h,$w]; break;
            }
        }
    }

    // Wyliczenie kadrowania
    $srcRatio = $w / $h; $dstRatio = $targetW / $targetH;
    if ($mode === 'cover') {
        if ($srcRatio > $dstRatio) {
            $newH = $h; $newW = (int) round($h * $dstRatio);
            $srcX = (int) round(($w - $newW) / 2); $srcY = 0;
        } else {
            $newW = $w; $newH = (int) round($w / $dstRatio);
            $srcX = 0; $srcY = (int) round(($h - $newH) / 2);
        }
        $copyW = $newW; $copyH = $newH;
    } else { // contain
        $srcX = 0; $srcY = 0; $copyW = $w; $copyH = $h;
        if ($srcRatio > $dstRatio) {
            $targetH = (int) round($targetW / $srcRatio);
        } else {
            $targetW = (int) round($targetH * $srcRatio);
        }
    }

    $dst = imagecreatetruecolor($targetW, $targetH);

    // Obsługa przezroczystości
    imagealphablending($dst, false);
    imagesavealpha($dst, true);
    $transparent = imagecolorallocatealpha($dst, 0, 0, 0, 127);
    imagefilledrectangle($dst, 0, 0, $targetW, $targetH, $transparent);

    imagecopyresampled($dst, $src, 0, 0, $srcX, $srcY, $targetW, $targetH, $copyW, $copyH);

    // Zapis
    $ok = false; $quality = 82;
    switch ($format) {
        case 'webp':
            if (function_exists('imagewebp')) { $ok = imagewebp($dst, $dstPath, $quality); }
            break;
        case 'jpg': case 'jpeg':
            imageinterlace($dst, 1);
            $ok = imagejpeg($dst, $dstPath, $quality);
            break;
        case 'png':
            $ok = imagepng($dst, $dstPath, 6);
            break;
    }

    imagedestroy($src); imagedestroy($dst);
    if (!$ok) { throw new RuntimeException('Nie udało się zapisać miniatury'); }
}

Ta funkcja wystarczy dla większości zastosowań: blog, portfolio, listing produktów. Jeśli przetwarzasz naprawdę duże pliki lub chcesz wyciskać maksymalną jakość, spójrz na Imagick.

Przykład z Imagick

Imagick to bardziej zaawansowane narzędzie o świetnych algorytmach skalowania i ostrzenia. Poniżej prosty przykład, który generuje miniaturę w trybie cover.

<?php
function createThumbnailImagick($srcPath, $dstPath, $targetW, $targetH, $format = 'webp') {
    $img = new Imagick($srcPath);

    // Orientacja i profil kolorów
    $orientation = $img->getImageOrientation();
    switch ($orientation) {
        case Imagick::ORIENTATION_RIGHTTOP:  $img->rotateimage('#000', 90); break;
        case Imagick::ORIENTATION_BOTTOMRIGHT:$img->rotateimage('#000', 180); break;
        case Imagick::ORIENTATION_LEFTBOTTOM: $img->rotateimage('#000', -90); break;
    }
    $img->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);

    // cover: przytnij do proporcji docelowych, potem skaluj
    $img->cropThumbnailImage($targetW, $targetH);

    // Opcjonalny lekki sharpening po skalowaniu
    $img->unsharpMaskImage(0.5, 0.5, 0.6, 0.05);

    $format = strtolower($format);
    if ($format === 'jpg') $format = 'jpeg';

    $img->setImageFormat($format);
    if (in_array($format, ['jpeg','webp','avif'])) {
        $img->setImageCompressionQuality(82);
    }

    // Zapis do pliku tymczasowego i atomowy rename
    $tmp = $dstPath . '.tmp';
    $img->writeImage($tmp);
    $img->destroy();
    rename($tmp, $dstPath);
}

W Imagick przy większych plikach zyskasz na jakości i czasie przetwarzania. Dodatkowo łatwo skorzystać z AVIF, jeśli Twój ImageMagick i biblioteki systemowe go obsługują.

Nazewnictwo i cache, czyli porządek musi być

Przemyślane nazewnictwo miniaturek pomaga zarówno programiście, jak i serwerowi cache. Popularny schemat: nazwa-800x600-cover.webp. Taki plik można zwracać prosto z Nginx z długim Cache-Control, bez angażowania PHP. Dla generacji „na żądanie” sprawdza się strategia try_files: jeśli miniatura istnieje – serwuj ją; jeśli nie – wywołaj skrypt generujący i zapisz wynik.

location /thumbs/ {
    try_files $uri /thumb.php?$args;
    add_header Cache-Control 'public, max-age=31536000, immutable';
}

W RemnetCMS stosujemy też cache-warmer: po dodaniu obrazu system zawczasu generuje kilka najpopularniejszych rozmiarów, żeby pierwsze odsłony były natychmiastowe.

Miniaturki responsywne w praktyce

Jedna miniatura to za mało dla współczesnych ekranów. Warto przygotować zestaw rozmiarów i wysyłać je przez srcset oraz sizes. Format WebP/AVIF można serwować w <picture> z automatycznym fallbackiem do JPEG/PNG.

<picture>
  <source type='image/avif' srcset='foto-400-cover.avif 400w, foto-800-cover.avif 800w, foto-1200-cover.avif 1200w' sizes='(max-width: 800px) 100vw, 800px'>
  <source type='image/webp' srcset='foto-400-cover.webp 400w, foto-800-cover.webp 800w, foto-1200-cover.webp 1200w' sizes='(max-width: 800px) 100vw, 800px'>
  <img src='foto-800-cover.jpg' srcset='foto-400-cover.jpg 400w, foto-800-cover.jpg 800w, foto-1200-cover.jpg 1200w' sizes='(max-width: 800px) 100vw, 800px' width='800' height='600' loading='lazy' decoding='async' alt='Opis zdjęcia'>
</picture>

Kluczowe detale: atrybuty width/height zapobiegają skakaniu układu (CLS), a loading="lazy" ogranicza transfer. W połączeniu z dobrym cache wyniki w Lighthouse robią się zielone jak wiosenna trawa.

Generować przy wgraniu czy „on the fly”?

Oba podejścia są poprawne – różnią się kosztami i wygodą.

  • Przy wgraniu: generujesz od razu kilka rozmiarów. Plusy – stałe czasy odpowiedzi i brak skoków obciążenia. Minus – dodatkowe miejsce na dysku.
  • On the fly: generujesz przy pierwszym żądaniu. Plus – oszczędzasz miejsce, tworzysz tylko potrzebne rozmiary. Minus – pierwszy użytkownik może poczekać ułamek sekundy.

W RemnetCMS zwykle łączymy oba światy: kluczowe rozmiary powstają od razu, a egzotyczne wymiary są tworzone „na żądanie” i zapisywane do cache. Dodatkowo generator działa w tle, by nie obciążać żądań użytkowników.

Jakość, kompresja i formaty: WebP i AVIF na prowadzeniu

Ustawienie jakości to delikatny balans. W praktyce 80–85 dla JPEG/WEBP jest bezpieczne, a dla PNG warto włączyć paletę (gd przy ograniczonej liczbie kolorów) lub przejść na WebP/AVIF. AVIF ma świetną kompresję przy zachowaniu jakości, ale jego kodowanie bywa wolniejsze i nie wszędzie jest dostępne – dobrym kompromisem bywa WebP jako domyślka i AVIF dla nowocześniejszych przeglądarek.

Bezpieczeństwo: nie tylko piksele

Obróbka obrazów to też wektor ataku, dlatego sprawdzaj rozszerzenia i MIME, narzucaj limity rozmiaru i pamięci, odrzucaj pliki z nietypowymi profilami ICC, a pliki tymczasowe zapisuj w katalogu bez prawa do wykonywania. Pamiętaj o nadpisaniu metadanych lub ich usunięciu, jeśli nie są potrzebne – to dodatkowe kilobajty mniej i mniej danych wrażliwych w plikach.

Autorski CMS vs WordPress: jak uniknąć „wtyczkowego” balastu

WordPress potrafi generować miniatury, ale w praktyce kończy się to dziesiątkami rozmiarów zdefiniowanych przez motyw i wtyczki, co spowalnia uploady i zjada miejsce. Do tego dochodzi brak spójnego pipeline’u obrazów oraz różna jakość kodowania między wtyczkami. W podejściu autorskim, które stosujemy w RemnetCMS, pipeline jest jeden, przewidywalny i szybki: minimalny kod po stronie klienta, zoptymalizowane algorytmy i zero nadmiarowych rozmiarów. Efekt? Stabilna wydajność, krótkie czasy TTFB i lepsze wyniki Core Web Vitals – bez doganiania problemów po każdej aktualizacji wtyczki.

Przykładowy workflow w RemnetCMS

  1. Walidacja obrazu i korekta EXIF.
  2. Natychmiastowa generacja 2–4 rozmiarów „złotych” (np. 400, 800, 1200 px szerokości).
  3. Zapisy atomowe i wersjonowanie nazw oparte na hashach, by uniknąć kolizji i wspierać cache immutable.
  4. Serwowanie z Nginx i długim Cache-Control; PHP omijamy w 99% żądań.
  5. Opcjonalna generacja „egzotycznych” miniaturek on the fly + zaplecione kolejkowanie w tle.

Taki układ zapewnia przewidywalne czasy odpowiedzi i pełną kontrolę nad jakością, bez niespodzianek w wydajności.

Typowe problemy i jak ich uniknąć

  • Pikseloza po skalowaniu: skaluj w dół etapami lub używaj algorytmów wyższej jakości (Imagick: Lanczos). Po skalowaniu lekki unsharp mask pomaga odzyskać wrażenie ostrości.
  • Miniatury „na boku”: zawsze uwzględnij EXIF albo wymuś neutralną orientację po rotacji.
  • Ciężkie strony kategorii: ogranicz do rzeczywistych rozmiarów widoku (np. 400–600 px dla kart) i włącz lazy-loading. Nie ładuj pełnych 3000 px w siatce miniaturek.
  • Zbyt wiele rozmiarów: trzymaj się kilku logicznych breakpointów. Każdy dodatkowy rozmiar to koszti miejsce na dysku.

Checklist: miniaturki PHP, które robią robotę

  • Wybierz bibliotekę: GD dla prostoty, Imagick dla jakości i dużych plików.
  • Ustal tryb: cover dla siatek, contain dla galerii mieszanych.
  • Obsłuż EXIF i przezroczystość.
  • Preferuj WebP/AVIF, ustaw rozsądną jakość (ok. 82).
  • Używaj srcset/sizes i atrybutów width/height.
  • Wdróż cache po stronie serwera (Nginx) i wersjonowanie nazw.
  • Generuj na uploadzie najważniejsze rozmiary, resztę „na żądanie”.
  • Testuj na realnych zdjęciach z aparatów i zrzutach ekranu – to inne profile i kompresja.

Gdy miniaturki stają się przewidywalne i lekkie, cała strona przyspiesza, a użytkownicy widzą to od pierwszego scrolla. A to zwykle przekłada się na więcej konwersji, lepszy SEO i spokojniejszy sen administratora.

Wpis w kategorii: Blog

Podziel się z nami opinią