Ta strona używa ciasteczek (cookies), dzięki którym nasz serwis może działać lepiej. Dowiedz się więcej OK, rozumiem
WebHelp.pl Warsztat Artykuły Generowanie miniatur z GD

Warsztat / Artykuły i tutoriale

Generowanie miniatur z GD

Rafał Kukawski 8 marca 2011 komentarze ()

Tym razem zajmiemy się czymś prostym - napiszemy skrypt, który z użyciem GD będzie generował miniatury zdjęć. Funkcjonalność ta przydaje się szczególnie w skryptach galerii zdjęć, uploadu plików, itp. PHP oferuje sporo funkcji, które pozwolą realizować zadanie na wiele różnych sposobów.

Na początek wypiszemy sobie założenia projektu:

  1. Skrypt ma tworzyć miniatury o określonych rozmiarach.
  2. Ma oferować różne możliwości radzenia sobie ze zdjęciami nieregularnych rozmiarów.
  3. Odczyt dowolnego formatu obsługiwanego przez GD.
  4. Zapis do JPG z możliwością ustawienia stopnia kompresji.
  5. Zapis do PNG z zachowaniem przezroczystości.
  6. Żadna operacja nie modyfikuje oryginalnego obrazu, tylko zwraca nowy obiekt Image z rezultatem ostatniej operacji.

Struktura

Skrypt będzie w postaci klasy Image, którą będziemy w przyszłych artykułach rozszerzać o kolejne możliwości. Konstruktor klasy będzie przyjmował ścieżkę do pliku lub zasób reprezentujący obraz.

Kod: Zaznacz cały
class Image {
    /**
     * Zasób obrazu, na którym będziemy wykonywać operacje
     * @protected
     * @type {resource}
     */
    protected $source;

    /**
     * Konstruktor klasy.
     * @constructor
     * @param {string|resource} $source Ścieżka do pliku z obrazem
     *     lub zasób reprezentujący obraz
     */
    public function __construct ($source) {
    }
}

Na początek musimy sprawdzić poprawność wprowadzonych danych do konstruktora i załadować obraz. W tym celu stworzymy dwie pomocnicze funkcje isImageResource oraz getImageType.

Kod: Zaznacz cały
static function isImageResource ($source) {
    return is_resource($source) && imagesx($source) !== false;
}

Funkcja sprawdzająca, czy podany argument jest zasobem obrazu opiera swoje działanie na funkcji imagesx. Jeśli funkcja będzie w stanie określić szerokość obrazu, możemy przyjąć, że mamy do czynienia z poprawnym zasobem GD. W przeciwnym wypadku funkcja zwróci false. Zamiast imagesx można skorzystać z funkcji get_resource_type.

Kod: Zaznacz cały
static function getImageType ($source) {
    $type = -1;

    if (is_string($source)) {
        $imageData = getimagesize($source);

        if ($imageData) {
            $type = $imageData[2];
        }
    }
    return $type;
}

Funkcja ustalająca typ obrazu obecnego pod podaną ścieżką korzysta z funkcji getimagesize. Paradoksalnie, funkcja ta nie tylko sprawdza wymiary obrazu ale dostarcza też informacji o typie obrazu rozpoznawanego przez GD. Jeśli wywołanie getimagesize zwróci false, znaczy to tyle, że plik nie istnieje lub plik nie jest obrazem rozpoznawanym przez GD. Wartość -1 zwrócona przez naszą funkcję oznacza błąd.

Wczytywanie obrazu

Kolejnym etapem jest wczytanie obrazu. Do wczytywania plików graficznych biblioteka GD definiuje funkcje imagecreatefrompng, imagecreatefromjpeg, imagecreatefromgif, itp. Jeśli chcielibyśmy skorzystać z tych funkcji, konieczne jest stworzenie sporej instrukcji warunkowej.

Kod: Zaznacz cały
$imageData = getimagesize($source);
$imageType = $imageData[2];

switch ($imageType) {
    case IMAGETYPE_JPEG:
        $image = imagecreatefromjpeg($source);
    break;
    case IMAGETYPE_PNG:
        $image = imagecreatefrompng($source);
    break;
    case IMAGETYPE_GIF:
        $image = imagecreatefromgif($source);
    break;
    // i tak dalej dla każdego obsługiwanego przez GD formatu
}

Nie jest to najwygodniejsze, ponieważ ręcznie listujemy wszystkie możliwe formaty. Sposób ten jest dobry, jeśli chcemy ograniczyć obsługiwane typy do kilku podstawowych formatów.

Istnieje jeszcze jeden sposób, który pozwoli nam otworzyć dowolny typ graficzny bez konieczności zbudowania powyższej instrukcji warunkowej. Jest nim funkcja imagecreatefromstring.

Na potrzeby artykułu skorzystamy z tej drugiej funkcji.

Kod: Zaznacz cały
protected function loadImage ($source) {
    $image = null;
    $type = -1;

    if (self::isImageResource($source)) {
        $image = $source;
    } else {
        $type = self::getImageType($source);

        if ($type !== -1) {
            $image = imagecreatefromstring(file_get_contents($source));
        }
    }

    return $image;
}

Gdy mamy już zaimplementowaną funkcję wczytującą obraz, możemy uzupełnić konstruktor.

Kod: Zaznacz cały
public function __construct ($source) {
    $image = $this->loadImage($source);

    if ($image) {
        $this->source = $image;
    } else {
        throw new InvalidArgumentException('Podane źródło nie jest obsługiwanym typem graficznym');
    }
}

Wymiary obrazu

Pomocne będą metody (lub bardziej intuicyjne gettery) do odczytu wysokości i szerokości obrazu. Dane te będą przydatne podczas skalowania oraz w przyszłych zastosowaniach.

Kod: Zaznacz cały
public function __get ($prop) {
    switch ($prop) {
        case 'width':
            return imagesx($this->source);
        case 'height':
            return imagesy($this->source);
    }
    return null;
}

Skalowanie obrazu

W poprzednich rozdziałach zajęliśmy się wczytaniem obrazu. Teraz można zająć się jego skalowaniem. Na początek przeskalujemy obraz do sztywnych rozmiarów.

Jako testowe zdjęcie posłuży nam Lena. Zdjęcie o rozmiarze 512/512 pikseli.

Lena

Pierwszym krokiem tworzenia miniatury będzie utworzenie nowego, pustego obrazu, o wymiarach, które chcemy uzyskać. Użyjemy tutaj funkcji imagecreatetruecolor.

Kod: Zaznacz cały
$newImage = imagecreatetruecolor($newWidth, $newHeight);

Skalowanie obrazu polegać będzie na przekopiowaniu obrazu źródłowego na obraz utworzony w poprzednim kroku, za pomocą funkcji imagecopyresized lub imagescopyresampled. Z racji tego, że resamplowanie zwraca wyniki lepszej jakości - dzięki użytym filtrom - niż zwykłe skalowanie, użyjemy drugiej funkcji, której sygnatura definiuje aż 10 argumentów.

Kod: Zaznacz cały
bool imagecopyresampled ( resource $dst_image , resource $src_image , int $dst_x , int $dst_y , int $src_x , int $src_y , int $dst_w , int $dst_h , int $src_w , int $src_h );

Pierwszy argument stanowi obraz na który chcemy kopiować fragment obrazu źródłowego, który z kolei przekażemy jako drugi argument. Argumenty 3, 4, 7 i 8 definiują obszar (prostokąt rozpoczynający się od współrzędnych $dst_x i $dst_y obrazu wynikowego oraz jego szerokość i wysokość) na obrazie wynikowym, w którym zostanie umieszczony wybrany fragment (argumenty 5, 6, 9 i 10) obrazu źródłowego. Jeśli obszar na nowym obrazie jest mniejszy bądź większy od obszaru obrazu źródłowego, dokonane zostanie przeskalowanie. W naszym zadaniu kopiujemy cały obszar obrazu źródłowego na cały obszar obrazu wynikowego. Zatem parametry 3, 4, 7 i 8 będą przyjmować kolejno wartości 0, 0, szerokość miniatury, wysokość miniatury, a argumenty 5, 6, 9 i 10 odpowiednio 0, 0, szerokość obrazu źródłowego i wysokość obrazu źródłowego.

Kod: Zaznacz cały
public function scale ($newWidth, $newHeight) {
    $newImage = imagecreatetruecolor($newWidth, $newHeight);
    imagecopyresampled($newImage, $this->source, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
    return new Image($newImage);
}

Poniżej przykładowy wynik użycia powyższej funkcji w porównaniu z użyciem funkcji imagecopyresized.

Lena resampled Lena resized

Różne wariacje tworzenia miniatur

Utworzenie miniatury o sztywnych rozmiarach było banalnie proste. Jednak zdjęcia, których proporcje nie odpowiadają dokładnie wielkościom miniatur zostaną rozciągnięte w poziomie lub pionie, tak jak zdjęcie Leny przeskalowane do 75/150 pikseli.

Skalowanie bez zachowania proporcji

W tym rozdziale użyjemy trochę prostej matematyki, żeby uzyskać różnego rodzaju proporcje szerokości do wysokości miniatury.

Wysokość proporcjonalna do szerokości

W przypadku, gdy chcemy, żeby wysokość miniatury zawsze zależała od jej szerokości, wystarczy przed wywołaniem imagecreatetruecolor obliczyć nową wysokość miniatury za pomocą wzoru:

Kod: Zaznacz cały
$newHeight = round($newWidth * $this->height / $this->width);

Na potrzeby dalszych opcji tę proporcję nazwijmy PROP_HEIGHT. Wynikiem wywołania $this->scale(100, 150, Image::PROP_HEIGHT) będzie obraz o rozmiarze 100/100px.

Wysokość proporcjonalnie do szerokości

Szerokość proporcjonalna do wysokości

W przypadku szerokości wystarczy nieco zmienić kolejność argumentów:

Kod: Zaznacz cały
$newWidth = round($newHeight * $this->width / $this->height);

Opcję nazwijmy PROP_WIDTH. Wynikiem wywołania $this->scale(100, 150, Image::PROP_WIDTH) będzie obraz o wymiarach 150/150px.

Szerokość proporcjonalnie do wysokości

Określenie maksymalnych wymiarów miniatury

Powyższe dwie wariacje sprawdzą się, jeśli chcemy uzyskać miniatury o określonej wysokości lub szerokości. Czasami jednak nie ma potrzeby usztywniać jednej krawędzi, tylko ważne jest, żeby miniatura nie przekroczyła określonych rozmiarów. Aby uzyskać taki rezultat należy porównać proporcje $newWidth / $width i $newHeight / $height i przemnożyć szerokość i wysokość przez mniejszą z wartości.

Kod: Zaznacz cały
$scale = min($newWidth / $this->width, $newHeight / $this->height);
$newWidth = round($this->width * $scale);
$newHeight = round($this->height * $scale);

Tej opcji nadamy nazwę PROP_SHRINK. Poprzez wywołanie $this->scale(100, 150, Image::PROP_SHRINK) uzyskamy obraz o wymiarach 100/100px.

PROP_SHRINK

Określenie minimalnych wymiarów miniatury

Można też odwrócić założenia poprzedniego punktu, czyli minimalny rozmiar dowolnej krawędzi ma być ustawiony na sztywno. W takim wypadku wymiary obliczymy mnożąc szerokość i wysokość przez większy współczynnik.

Kod: Zaznacz cały
$scale = max($newWidth / $this->width, $newHeight / $this->height);
$newWidth = round($this->width * $scale);
$newHeight = round($this->height * $scale);

Odpowiednią nazwą będzie PROP_GROW. Wywołując funkcję z proporcją $this->scale(100, 150, Image::PROP_GROW) otrzymamy obraz o wymiarach 150/150px.

PROP_GROW

Równomierne skalowanie do sztywnych wymiarów

Tworzenie tego typu miniatury może przebiegać dwuetapowo. W pierwszym kroku tworzymy miniaturę zgodnie z proporcją PROP_SHRINK.

Kod: Zaznacz cały
$scaled = $this->scale($newWidth, $newHeight, Image::PROP_SHRINK);

Następnie tworzymy pusty obraz o porządanej wielkości miniatury i wklejamy w jego środek wcześniej utworzoną miniaturę. Użyjemy tutaj funkcji, której opis znajdziesz w dalszej części artykułu.

Kod: Zaznacz cały
$empty = new Image(imagecreatetruecolor($newWidth, $newHeight));
$fitted = $empty->merge($scaled, Image::HCENTER | Image::VCENTER);

Wynikiem działania tej funkcji dla wymiarów 100/150px będzie poniższy obraz.

PROP_FIT

Proszę zauważyć puste fragmenty nad i pod zdjęciem.

Maksymalne wypełnienie do podanych wymiarów. Przycinanie nadmiaru

Aby uzyskać obraz maksymalnie wypełniający obszar z przyciętym nadmiarem, także można działać dwuetapowo.

Najpierw trzeba przeskalować zdjęcie, zgodnie z opcją PROP_GROW.

Kod: Zaznacz cały
$scaled = $this->scale($newWidth, $newHeight, Image::PROP_GROW);

W drugim kroku skadrujemy zdjęcie.

Kod: Zaznacz cały
$cropped = $scaled->crop($newWidth, $newHeight, Image::HCENTER | Image::VCENTER);

Wywołanie funkcji z parametrami $this->scale(100, 150, Image::PROP_CROP) zwróci obraz, który w porównaniu z oryginałem jest przycięty z lewej i prawej strony.

PROP_CROP

Finalna wersja funkcji skalującej

Wszystkie sposoby warto umieścić w jednej funkcji, aby możliwa była zmiana sposobu skalowania poprzez parametry. Zdefiniujemy zatem funkcję o 3 argumentach, gdzie pierwsze 2 stanowić będą rozmiar miniatury, zaś ostatni tryb skalowania.

Kod: Zaznacz cały
public function scale ($newWidth, $newHeight, $proportions = 0) {
    $width = $this->width; // cache’ujemy rozmiary obrazu, żeby zbyt często nie korzystać z gettera
    $height = $this->height;

    // Sprawdzamy, czy zdjęcie jest poziome
    $landscape = $width > $height;

    switch ($proportions) {
        case self::PROP_FIT:
            $scaled = $this->scale($newWidth, $newHeight, self::PROP_SHRINK);
            $tmp = new Image(imagecreatetruecolor($newWidth, $newHeight));
            
            return $tmp->merge($scaled, self::HCENTER | self::VCENTER);
        case self::PROP_CROP:
            $scaled = $this->scale($newWidth, $newHeight, self::PROP_GROW);
            return $scaled->crop($newWidth, $newHeight, self::HCENTER | self::VCENTER);
        case self::PROP_SHRINK:
            $scale = min($newWidth / $width, $newHeight / $height);
            
            $newWidth = round($width * $scale);
            $newHeight = round($height * $scale);
            return $this->scale($newWidth, $newHeight, Image::PROP_NONE);
        case self::PROP_GROW:
            $scale = max($newWidth / $width, $newHeight / $height);
            
            $newWidth = round($width * $scale);
            $newHeight = round($height * $scale);
            return $this->scale($newWidth, $newHeight, Image::PROP_NONE);
        case self::PROP_HEIGHT:
            $newHeight = round($newWidth * $height / $width);
            return $this->scale($newWidth, $newHeight, self::PROP_NONE);
        case self::PROP_WIDTH:
            $newWidth = round($newHeight * $width / $height);
            return $this->scale($newWidth, $newHeight, self::PROP_NONE);
        default:
            $newImage = imagecreatetruecolor($newWidth, $newHeight);
            imagecopyresampled($newImage, $this->source, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);

            return new Image($newImage);
    }
}

Tryby skalowania zdefiniujemy jako stałe klasy.

Kod: Zaznacz cały
const PROP_NONE = 0;
const PROP_HEIGHT = 1;
const PROP_WIDTH = 2;
const PROP_SHRINK = 3;
const PROP_GROW = 4;
const PROP_FIT = 5;
const PROP_CROP = 6;

Kadrowanie zdjęć

Jedna z opcji skalowania wymagała możliwości kadrowania zdjęć. Poniższa funkcja pomoże zrealizować zadanie.

Kod: Zaznacz cały
public function crop ($toWidth, $toHeight, $align = 0) {
    $newImage = imagecreatetruecolor($toWidth, $toHeight);
    $width = $this->width;
    $height = $this->height;

    // Jeśli obraz węższy od kadru, przyjmujemy jego szerokość
    $toWidth = min($width, $toWidth);

    // Jeśli obraz niższy od kadru, przyjmujemy jego wysokość
    $toHeight = min($height, $toHeight);

    $x = 0; $y = 0;

    if (0 !== ($align & self::RIGHT)) {
        $x = $width - $toWidth;
    } else if (0 !== ($align & self::HCENTER)) {
        $x = floor(($width - $toWidth) / 2);
    }

    if (0 !== ($align & self::BOTTOM)) {
        $y = $height - $toHeight;
    } else if (0 !== ($align & self::VCENTER)) {
        $y = floor(($height - $toHeight) / 2);
    }

    imagecopy($newImage, $this->source, 0, 0, $x, $y, $toWidth, $toHeight);

    return new Image($newImage);
}

Ostatni parametr funkcji oznacza pozycję zdjęcia, która będzie widoczna na skadrowanym obrazie. Przykładowo VCENTER | HCENTER oznacza, że w kadrze znajdzie się środek zdjęcia. BOTTOM | RIGHT umieszcza wewnątrz kadru prawy dolny róg obrazu.

Przykładowe wywołanie $this->crop(250, 250, Image::VCENTER | Image::HCENTER) zwróci obraz przycięty z każdej strony do 250 pikseli.

Skadrowany obraz Leny

Wklejanie obrazu do drugiego

Podobnie jak w przypadku kadrowania, możliwość wklejania jednego obrazu w drugim jest niezbędna dla jednej opcji tworzenia miniatur. Funkcja będzie oczekiwała 4 argumentów, z czego 3 są opcjonalne. Pierwszym argumentem będzie obraz do wklejenia. Drugi stanowi pozycję oryginalnego zdjęcia, gdzie chcemy wkleić. Możliwe wartości zdefiniowane są w stałych TOP, BOTTOM, VCENTER, LEFT, RIGHT, HCENTER. Przykładowo, aby wyrółnać wklejane zdjęcie do prawego dolnego rogu, należy do funkcji przekazać wartość Image::BOTTOM | Image::RIGHT. Jak widać, korzystamy z operatora bitowego OR.

Parametry vmargin i hmargin pomogą odsunąć wklejone zdjęcie od krawędzi o podaną ilość pikseli. Wartość jest ignorowana dla wyrównania do środka (VCENTER i HCENTER).

Kod: Zaznacz cały
public function merge ($image, $align = 0, $hmargin = 0, $vmargin = 0) {
    if (self::isImageResource($image)) {
        $image = new Image($image);
    } else if (!($image instanceOf Image)) {
        throw new InvalidArgumentException('Oczekiwano instancji klasy Image lub zasobu z obrazem');
    }

    $width = $this->width;
    $height = $this->height;

    $imageWidth = $image->width;
    $imageHeight = $image->height;

    $x = $hmargin; $y = $vmargin;

    if (0 !== ($align & self::RIGHT)) {
        $x = $width - $imageWidth - $hmargin;
    } else if (0 !== ($align & self::HCENTER)) {
        $x = round(($width - $imageWidth) / 2);
    }

    if (0 !== ($align & self::BOTTOM)) {
        $y = $height - $imageHeight - $vmargin;
    } else if (0 !== ($align & self::VCENTER)) {
        $y = round(($height - $imageHeight) / 2);
    }

    $newImage = imagecreatetruecolor($width, $height);
    imagecopy($newImage, $this->source, 0, 0, 0, 0, $width, $height);
    imagecopy($newImage, $image->rawImage, $x, $y, 0, 0, $imageWidth, $imageHeight);

    return new Image($newImage);
}

Przykładowo, jeśli połączymy zdjęcie kolorowe z szarym poprzez wywołania

Kod: Zaznacz cały
$img = new Image('lena_color.png');
$gray = new Image('lena_gray.png');
$width = round($img->width / 3);
$height = round($img->height / 3);
$img = $img->merge($gray->crop($width, $height, Image::TOP | Image::LEFT), Image::TOP | Image::LEFT);
$img = $img->merge($gray->crop($width, $height, Image::TOP | Image::RIGHT), Image::TOP | Image::RIGHT);
$img = $img->merge($gray->crop($width, $height, Image::VCENTER | Image::HCENTER), Image::VCENTER | Image::HCENTER);
$img = $img->merge($gray->crop($width, $height, Image::BOTTOM | Image::LEFT), Image::BOTTOM | Image::LEFT);
$img = $img->merge($gray->crop($width, $height, Image::BOTTOM | Image::RIGHT), Image::BOTTOM | Image::RIGHT);
$th = $img->scale(250, 250);

uzyskamy poniższą szachownicę.

Szachownica

Stałe

Poniżej stałe przydatne w kadrowaniu i wklejaniu zdjęć

Kod: Zaznacz cały
const BOTTOM = 32;
const TOP = 16;
const RIGHT = 8;
const LEFT = 4;
const VCENTER = 2;
const HCENTER = 1;

Dostęp do surowych danych

Aby powyższa funkcja merge poprawnie działała, musimy udostępnić surowy dane wklejanego obrazu. W tym celu zaoferujemy API w postaci kolejnego gettera o nazwie rawImage.

Kod: Zaznacz cały
public function __get ($prop) {
    switch ($prop) {
        case 'width':
            return imagesx($this->source);
        case 'height':
            return imagesy($this->source);
        case 'rawImage':
            return $this->source;
    }
    return null;
}

Zapis obrazu do pliku

Założenia projektu przewidują zapis tylko do formatów JPG i PNG, dlatego utworzymy dwie metody realizujące operację zapisu.

Kod: Zaznacz cały
public function toPNG ($dest, $compression = 0, $filters = PNG_NO_FILTER) {
    return imagepng($this->source, $dest, $compression, $filters);
}

public function toJPG ($dest, $quality = 75) {
    return imagejpeg($this->source, $dest, $quality);
}

Zachowanie przezroczystości PNG

Żadna z wyżej zaimplementowanych funkcji nie zachowuje przezroczystości plików PNG, choć takie były założenia projektu. Aby to naprawić, wystarczy w miejscach, gdzie tworzymy nowy, pusty obraz za pomocą imagecreatetruecolor wywołać jeszcze kilka poleceń. Żeby nie zaśmiecać kilku miejsc powtarzającymi się poleceniami, zdefiniujemy sobie statyczną funkcję, która zwróci przezroczysty obraz o podanych rozmiarach, który będzie miał włączone zapamiętywanie kanału alpha.

Kod: Zaznacz cały
protected static function transparent ($width, $height) {
    $image = imagecreatetruecolor($width, $height);
    imagealphablending($image, false);
    imagesavealpha($image, true);
    $color = imagecolortransparent($image, imagecolorallocatealpha($image, 0, 0, 0, 127));
    imagefill($image, 0, 0, $color);
       
    return $image;
}

Wszystkie wystąpienia imagecreatetruecolor zamieniamy na

Kod: Zaznacz cały
Image::transparent($width, $height);

Kompletny kod

Na tym zakończymy prace nad pierwszą wersją klasy Image. Kompletny kod można pobrać tutaj.

Masz pytania lub wątpliwości? Odwiedź nasze forum dyskusyjne.

Rafał Kukawski

Programista, webmaster. Szczególnie upodobał sobie JavaScript i technologie klienckie, choć strona serwera i bazy danych nie stanowią tajemnicy. Tworzy też aplikacje na urządzenia mobilne. kukawski.pl.


Komentarze


HTML CSS JavaScript PHP bazy danych MySQL Flash grafika framework hosting domeny pozycjonowanie wordpress Facebook