Efektywna zmiana rozmiaru obrazu JPEG w PHP


82

Jaki jest najskuteczniejszy sposób zmiany rozmiaru dużych obrazów w PHP?

Obecnie używam funkcji GD imagecopyresampled do robienia zdjęć w wysokiej rozdzielczości i czystego zmniejszania ich rozmiaru do rozmiaru do przeglądania w Internecie (około 700 pikseli szerokości i 700 pikseli wysokości).

Działa to świetnie w przypadku małych (poniżej 2 MB) zdjęć, a cała operacja zmiany rozmiaru zajmuje mniej niż sekundę na serwerze. Jednak witryna docelowo będzie obsługiwać fotografów, którzy mogą przesyłać zdjęcia o rozmiarze do 10 MB (lub obrazy o rozmiarze do 5000x4000 pikseli).

Wykonywanie tego rodzaju operacji zmiany rozmiaru z dużymi obrazami ma tendencję do zwiększania zużycia pamięci o bardzo duży margines (większe obrazy mogą zwiększyć użycie pamięci dla skryptu powyżej 80 MB). Czy istnieje sposób, aby operacja zmiany rozmiaru była bardziej wydajna? Czy powinienem używać alternatywnej biblioteki obrazów, takiej jak ImageMagick ?

W tej chwili kod zmiany rozmiaru wygląda mniej więcej tak

function makeThumbnail($sourcefile, $endfile, $thumbwidth, $thumbheight, $quality) {
    // Takes the sourcefile (path/to/image.jpg) and makes a thumbnail from it
    // and places it at endfile (path/to/thumb.jpg).

    // Load image and get image size.
    $img = imagecreatefromjpeg($sourcefile);
    $width = imagesx( $img );
    $height = imagesy( $img );

    if ($width > $height) {
        $newwidth = $thumbwidth;
        $divisor = $width / $thumbwidth;
        $newheight = floor( $height / $divisor);
    } else {
        $newheight = $thumbheight;
        $divisor = $height / $thumbheight;
        $newwidth = floor( $width / $divisor );
    }

    // Create a new temporary image.
    $tmpimg = imagecreatetruecolor( $newwidth, $newheight );

    // Copy and resize old image into new image.
    imagecopyresampled( $tmpimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height );

    // Save thumbnail into a file.
    imagejpeg( $tmpimg, $endfile, $quality);

    // release the memory
    imagedestroy($tmpimg);
    imagedestroy($img);

Odpowiedzi:


45

Ludzie mówią, że ImageMagick jest znacznie szybszy. W najlepszym przypadku wystarczy porównać obie biblioteki i zmierzyć to.

  1. Przygotuj 1000 typowych obrazów.
  2. Napisz dwa skrypty - jeden dla GD, drugi dla ImageMagick.
  3. Uruchom oba kilka razy.
  4. Porównaj wyniki (całkowity czas wykonania, wykorzystanie procesora i we / wy, jakość obrazu wyniku).

Coś, co najlepsze dla wszystkich, nie może być dla Ciebie najlepsze.

Ponadto, moim zdaniem, ImageMagick ma znacznie lepszy interfejs API.


2
Na serwerach, z którymi pracowałem, GD często kończy się brakiem pamięci RAM i zawiesza się, podczas gdy ImageMagick nigdy tego nie robi.
Abhi Beckert,

Nie mogę się więcej nie zgodzić. Uważam, że imagemagick jest koszmarem do pracy. Często otrzymuję 500 błędów serwera dla dużych obrazów. Trzeba przyznać, że biblioteka GD uległaby awarii wcześniej. Ale nadal mówimy czasami tylko o obrazach 6 Mb, a 500 błędów to po prostu najgorsze.
Single Entity

20

Oto fragment z dokumentów php.net, których użyłem w projekcie i działa dobrze:

<?
function fastimagecopyresampled (&$dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h, $quality = 3) {
    // Plug-and-Play fastimagecopyresampled function replaces much slower imagecopyresampled.
    // Just include this function and change all "imagecopyresampled" references to "fastimagecopyresampled".
    // Typically from 30 to 60 times faster when reducing high resolution images down to thumbnail size using the default quality setting.
    // Author: Tim Eckel - Date: 09/07/07 - Version: 1.1 - Project: FreeRingers.net - Freely distributable - These comments must remain.
    //
    // Optional "quality" parameter (defaults is 3). Fractional values are allowed, for example 1.5. Must be greater than zero.
    // Between 0 and 1 = Fast, but mosaic results, closer to 0 increases the mosaic effect.
    // 1 = Up to 350 times faster. Poor results, looks very similar to imagecopyresized.
    // 2 = Up to 95 times faster.  Images appear a little sharp, some prefer this over a quality of 3.
    // 3 = Up to 60 times faster.  Will give high quality smooth results very close to imagecopyresampled, just faster.
    // 4 = Up to 25 times faster.  Almost identical to imagecopyresampled for most images.
    // 5 = No speedup. Just uses imagecopyresampled, no advantage over imagecopyresampled.

    if (empty($src_image) || empty($dst_image) || $quality <= 0) { return false; }
    if ($quality < 5 && (($dst_w * $quality) < $src_w || ($dst_h * $quality) < $src_h)) {
        $temp = imagecreatetruecolor ($dst_w * $quality + 1, $dst_h * $quality + 1);
        imagecopyresized ($temp, $src_image, 0, 0, $src_x, $src_y, $dst_w * $quality + 1, $dst_h * $quality + 1, $src_w, $src_h);
        imagecopyresampled ($dst_image, $temp, $dst_x, $dst_y, 0, 0, $dst_w, $dst_h, $dst_w * $quality, $dst_h * $quality);
        imagedestroy ($temp);
    } else imagecopyresampled ($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
    return true;
}
?>

http://us.php.net/manual/en/function.imagecopyresampled.php#77679


Czy wiesz, co byś wstawił dla $ dst_x, $ dst_y, $ src_x, $ src_y?
JasonDavis

Jeżeli nie zastąpi $quality + 1z ($quality + 1)? W obecnej sytuacji po prostu zmieniasz rozmiar za pomocą bezużytecznego dodatkowego piksela. Gdzie jest kontrola zwarcia, kiedy $dst_w * $qualityjest> $src_w?
Walf

8
Skopiuj / wklej z sugerowanej edycji: To jest Tim Eckel, autor tej funkcji. Jakość $ + 1 jest poprawna, służy do uniknięcia czarnej ramki o szerokości jednego piksela, a nie do zmiany jakości. Ponadto ta funkcja jest wtyczką kompatybilna z imagecopyresampled, więc w przypadku pytań dotyczących składni zobacz polecenie imagecopyresampled, jest identyczne.
Andomar,

w czym to rozwiązanie jest lepsze od proponowanego w pytaniu? nadal używasz biblioteki GD z tymi samymi funkcjami.
TMS

1
@Tomas, właściwie, to też używa imagecopyresized(). Zasadniczo polega to najpierw na zmianie rozmiaru obrazu do rozsądnego rozmiaru ( final dimensionspomnożonego przez quality), a następnie ponowne próbkowanie, a nie po prostu ponowne próbkowanie obrazu w pełnym rozmiarze. Może to skutkować niższą jakością obrazu końcowego, ale zużywa znacznie mniej zasobów dla większych obrazów niż imagecopyresampled()sam, ponieważ algorytm ponownego próbkowania musi domyślnie radzić sobie tylko z obrazem o rozmiarze 3x ostatecznych wymiarów, w porównaniu do obrazu w pełnym rozmiarze ( który może być znacznie większy, szczególnie w przypadku zdjęć, których rozmiar jest zmieniany w przypadku miniatur).
0b10011

12

phpThumb używa ImageMagicka, gdy tylko jest to możliwe dla szybkości (w razie potrzeby cofa się do GD) i wydaje się, że całkiem dobrze buforuje, aby zmniejszyć obciążenie serwera. Jest dość lekki do wypróbowania (aby zmienić rozmiar obrazu, po prostu wywołaj phpThumb.php z zapytaniem GET, które zawiera nazwę pliku graficznego i wymiary wyjściowe), więc możesz dać mu szansę, aby sprawdzić, czy spełnia Twoje potrzeby.


ale to nie jest częścią standardowego PHP, jak się wydaje ... więc nie będzie dostępne na większości hostingu :(
TMS

1
wydaje mi się, że to tylko skrypt php, wystarczy mieć php gd i imagemagick
Flo

Rzeczywiście jest to skrypt PHP, a nie rozszerzenie, które musisz zainstalować, więc jest dobre dla współdzielonych środowisk hostingowych. Wystąpił błąd „Wyczerpano dozwolony rozmiar pamięci N bajtów” podczas próby przesłania obrazów JPEG <1 MB o wymiarach 4000x3000. Użycie phpThumb (a tym samym ImageMagick) rozwiązało problem i było bardzo łatwe do włączenia do mojego kodu.
w5m

10

W przypadku większych obrazów użyj libjpeg, aby zmienić rozmiar podczas ładowania obrazu w ImageMagick, a tym samym znacznie zmniejszyć zużycie pamięci i poprawić wydajność, nie jest to możliwe w przypadku GD.

$im = new Imagick();
try {
  $im->pingImage($file_name);
} catch (ImagickException $e) {
  throw new Exception(_('Invalid or corrupted image file, please try uploading another image.'));
}

$width  = $im->getImageWidth();
$height = $im->getImageHeight();
if ($width > $config['width_threshold'] || $height > $config['height_threshold'])
{
  try {
/* send thumbnail parameters to Imagick so that libjpeg can resize images
 * as they are loaded instead of consuming additional resources to pass back
 * to PHP.
 */
    $fitbyWidth = ($config['width_threshold'] / $width) > ($config['height_threshold'] / $height);
    $aspectRatio = $height / $width;
    if ($fitbyWidth) {
      $im->setSize($config['width_threshold'], abs($width * $aspectRatio));
    } else {
      $im->setSize(abs($height / $aspectRatio), $config['height_threshold']);
    }
    $im->readImage($file_name);

/* Imagick::thumbnailImage(fit = true) has a bug that it does fit both dimensions
 */
//  $im->thumbnailImage($config['width_threshold'], $config['height_threshold'], true);

// workaround:
    if ($fitbyWidth) {
      $im->thumbnailImage($config['width_threshold'], 0, false);
    } else {
      $im->thumbnailImage(0, $config['height_threshold'], false);
    }

    $im->setImageFileName($thumbnail_name);
    $im->writeImage();
  }
  catch (ImagickException $e)
  {
    header('HTTP/1.1 500 Internal Server Error');
    throw new Exception(_('An error occured reszing the image.'));
  }
}

/* cleanup Imagick
 */
$im->destroy();

9

Z twojego pytania, wygląda na to, że jesteś trochę nowy w GD, podzielę się swoim doświadczeniem, może to trochę nie na temat, ale myślę, że będzie to pomocne dla kogoś nowego w GD jak ty:

Krok 1, zweryfikuj plik. Użyj następującej funkcji, aby sprawdzić, czy $_FILES['image']['tmp_name']plik jest prawidłowym plikiem:

   function getContentsFromImage($image) {
      if (@is_file($image) == true) {
         return file_get_contents($image);
      } else {
         throw new \Exception('Invalid image');
      }
   }
   $contents = getContentsFromImage($_FILES['image']['tmp_name']);

Krok 2, pobierz format pliku Wypróbuj następującą funkcję z rozszerzeniem finfo, aby sprawdzić format pliku (zawartość). Powiedziałbyś, dlaczego nie użyjesz po prostu $_FILES["image"]["type"]do sprawdzenia formatu pliku? Ponieważ sprawdza TYLKO rozszerzenie pliku, a nie zawartość pliku, jeśli ktoś zmieni nazwę pliku pierwotnie nazwanego świat.png na świat.jpg , $_FILES["image"]["type"]zwróci jpeg, a nie png, więc $_FILES["image"]["type"]może zwrócić zły wynik.

   function getFormatFromContents($contents) {
      $finfo = new \finfo();
      $mimetype = $finfo->buffer($contents, FILEINFO_MIME_TYPE);
      switch ($mimetype) {
         case 'image/jpeg':
            return 'jpeg';
            break;
         case 'image/png':
            return 'png';
            break;
         case 'image/gif':
            return 'gif';
            break;
         default:
            throw new \Exception('Unknown or unsupported image format');
      }
   }
   $format = getFormatFromContents($contents);

Krok 3, Pobierz zasób GD Pobierz zasób GD z wcześniejszej zawartości:

   function getGDResourceFromContents($contents) {
      $resource = @imagecreatefromstring($contents);
      if ($resource == false) {
         throw new \Exception('Cannot process image');
      }
      return $resource;
   }
   $resource = getGDResourceFromContents($contents);

Krok 4, uzyskaj wymiar obrazu Teraz możesz uzyskać wymiar obrazu za pomocą następującego prostego kodu:

  $width = imagesx($resource);
  $height = imagesy($resource);

Zobaczmy teraz, jaką zmienną otrzymaliśmy z oryginalnego obrazu:

       $contents, $format, $resource, $width, $height
       OK, lets move on

Krok 5, oblicz argumenty obrazu o zmienionym rozmiarze Ten krok jest związany z twoim pytaniem, celem poniższej funkcji jest uzyskanie argumentów zmiany rozmiaru dla funkcji GD imagecopyresampled(), kod jest trochę długi, ale działa świetnie, ma nawet trzy opcje: stretch, shrink i wypełnij.

stretch : rozmiar obrazu wyjściowego jest taki sam, jak nowy ustawiony wymiar. Nie zachowa stosunku wysokości do szerokości.

shrink : rozmiar obrazu wyjściowego nie przekroczy nowego wymiaru, który podasz, i zachowaj stosunek wysokości do szerokości obrazu.

fill : rozmiar obrazu wyjściowego będzie taki sam jak nowy wymiar, który podasz, wrazie potrzeby przycina i zmienia rozmiar obrazu, zachowując stosunek wysokości do szerokości. Ta opcja jest tym, czego potrzebujesz w swoim pytaniu.

   function getResizeArgs($width, $height, $newwidth, $newheight, $option) {
      if ($option === 'stretch') {
         if ($width === $newwidth && $height === $newheight) {
            return false;
         }
         $dst_w = $newwidth;
         $dst_h = $newheight;
         $src_w = $width;
         $src_h = $height;
         $src_x = 0;
         $src_y = 0;
      } else if ($option === 'shrink') {
         if ($width <= $newwidth && $height <= $newheight) {
            return false;
         } else if ($width / $height >= $newwidth / $newheight) {
            $dst_w = $newwidth;
            $dst_h = (int) round(($newwidth * $height) / $width);
         } else {
            $dst_w = (int) round(($newheight * $width) / $height);
            $dst_h = $newheight;
         }
         $src_x = 0;
         $src_y = 0;
         $src_w = $width;
         $src_h = $height;
      } else if ($option === 'fill') {
         if ($width === $newwidth && $height === $newheight) {
            return false;
         }
         if ($width / $height >= $newwidth / $newheight) {
            $src_w = (int) round(($newwidth * $height) / $newheight);
            $src_h = $height;
            $src_x = (int) round(($width - $src_w) / 2);
            $src_y = 0;
         } else {
            $src_w = $width;
            $src_h = (int) round(($width * $newheight) / $newwidth);
            $src_x = 0;
            $src_y = (int) round(($height - $src_h) / 2);
         }
         $dst_w = $newwidth;
         $dst_h = $newheight;
      }
      if ($src_w < 1 || $src_h < 1) {
         throw new \Exception('Image width or height is too small');
      }
      return array(
          'dst_x' => 0,
          'dst_y' => 0,
          'src_x' => $src_x,
          'src_y' => $src_y,
          'dst_w' => $dst_w,
          'dst_h' => $dst_h,
          'src_w' => $src_w,
          'src_h' => $src_h
      );
   }
   $args = getResizeArgs($width, $height, 150, 170, 'fill');

Krok 6, zmiana rozmiaru obrazu Użyj $args, $width, $height, $formati $ zasób dostaliśmy od góry do następujących funkcji i uzyskać nowy zasób o zmienionym rozmiarze:

   function runResize($width, $height, $format, $resource, $args) {
      if ($args === false) {
         return; //if $args equal to false, this means no resize occurs;
      }
      $newimage = imagecreatetruecolor($args['dst_w'], $args['dst_h']);
      if ($format === 'png') {
         imagealphablending($newimage, false);
         imagesavealpha($newimage, true);
         $transparentindex = imagecolorallocatealpha($newimage, 255, 255, 255, 127);
         imagefill($newimage, 0, 0, $transparentindex);
      } else if ($format === 'gif') {
         $transparentindex = imagecolorallocatealpha($newimage, 255, 255, 255, 127);
         imagefill($newimage, 0, 0, $transparentindex);
         imagecolortransparent($newimage, $transparentindex);
      }
      imagecopyresampled($newimage, $resource, $args['dst_x'], $args['dst_y'], $args['src_x'], $args['src_y'], $args['dst_w'], $args['dst_h'], $args['src_w'], $args['src_h']);
      imagedestroy($resource);
      return $newimage;
   }
   $newresource = runResize($width, $height, $format, $resource, $args);

Krok 7, pobierz nową zawartość , użyj następującej funkcji, aby pobrać zawartość z nowego zasobu GD:

   function getContentsFromGDResource($resource, $format) {
      ob_start();
      switch ($format) {
         case 'gif':
            imagegif($resource);
            break;
         case 'jpeg':
            imagejpeg($resource, NULL, 100);
            break;
         case 'png':
            imagepng($resource, NULL, 9);
      }
      $contents = ob_get_contents();
      ob_end_clean();
      return $contents;
   }
   $newcontents = getContentsFromGDResource($newresource, $format);

Krok 8 pobierz rozszerzenie , użyj następującej funkcji, aby uzyskać rozszerzenie z formatu obrazu (uwaga, format obrazu nie jest równy rozszerzeniu obrazu):

   function getExtensionFromFormat($format) {
      switch ($format) {
         case 'gif':
            return 'gif';
            break;
         case 'jpeg':
            return 'jpg';
            break;
         case 'png':
            return 'png';
      }
   }
   $extension = getExtensionFromFormat($format);

Krok 9 zapisz obraz Jeśli mamy użytkownika o imieniu mike, możesz wykonać następujące czynności, zapisze on w tym samym folderze co ten skrypt php:

$user_name = 'mike';
$filename = $user_name . '.' . $extension;
file_put_contents($filename, $newcontents);

Krok 10 zniszcz zasoby Nie zapomnij zniszczyć zasobów GD!

imagedestroy($newresource);

lub możesz napisać cały swój kod w klasie i po prostu użyć następującego:

   public function __destruct() {
      @imagedestroy($this->resource);
   }

WSKAZÓWKI

Nie radzę konwertować formatu pliku przesłanego przez użytkownika, napotkasz wiele problemów.


4

Sugeruję, abyś popracował nad tym w następujący sposób:

  1. Wykonaj getimagesize () na przesłanym pliku, aby sprawdzić typ i rozmiar obrazu
  2. Zapisz każdy przesłany obraz JPEG mniejszy niż 700x700px w folderze docelowym „tak jak jest”
  3. Użyj biblioteki GD dla obrazów o średnim rozmiarze (zobacz ten artykuł, aby zapoznać się z przykładowym kodem: Zmiana rozmiaru obrazów za pomocą PHP i biblioteki GD )
  4. Użyj ImageMagick do dużych obrazów. Jeśli wolisz, możesz użyć ImageMagick w tle.

Aby używać programu ImageMagick w tle, przenieś przesłane pliki do folderu tymczasowego i zaplanuj zadanie CRON, które „konwertuje” wszystkie pliki do formatu jpeg i odpowiednio zmieni ich rozmiar. Zobacz składnię poleceń pod adresem: imagemagick-command line processing

Możesz zapytać użytkownika, że ​​plik został przesłany i zaplanowany do przetworzenia. Zadanie CRON można zaplanować na codzienne uruchamianie w określonych odstępach czasu. Obraz źródłowy można usunąć po przetworzeniu, aby mieć pewność, że nie zostanie przetworzony dwukrotnie.


Nie widzę powodu dla punktu 3 - użyj GD dla średnich rozmiarów. Dlaczego nie skorzystać z programu ImageMagick również do nich? To znacznie uprościłoby kod.
TMS

Znacznie lepszy niż cron byłby skrypt, który używa inotifywait, dzięki czemu zmiana rozmiaru rozpocznie się natychmiast, zamiast czekać na rozpoczęcie zadania crona.
ColinM,

3

Słyszałem wiele rzeczy o bibliotece Imagick, niestety nie mogłem jej zainstalować na swoim komputerze w pracy ani w domu (i wierz mi, spędziłem wiele godzin na wszelkiego rodzaju forach).

Afterwords zdecydowałem się wypróbować tę klasę PHP:

http://www.verot.net/php_class_upload.htm

Jest całkiem fajny i mogę zmienić rozmiar wszystkich rodzajów obrazów (mogę je również przekonwertować na JPG).


3

ImageMagick jest wielowątkowy, więc wydaje się być szybszy, ale w rzeczywistości zużywa o wiele więcej zasobów niż GD. Jeśli uruchomiłeś równolegle kilka skryptów PHP, wszystkie przy użyciu GD, pokonałyby one ImageMagick w szybkości dla prostych operacji. ExactImage jest mniej wydajne niż ImageMagick, ale dużo szybsze, chociaż nie jest dostępne w PHP, musisz je zainstalować na serwerze i uruchomić exec.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.