Правильный ресайз прозрачных картинок на PHP

26 комментариев

Черный фон при ресайзе прозрачных картинокЯ думаю каждый веб программист сталкивался с задачей ресайза картинок. Впрочем это касается не только ресайза, а любой обработки изображений в своих проектах. И думаю каждый уже имеет свой небольшой велосипед, класс, функцию которая ресайзит картинки. Но задумывались ли вы о том, что ваш велосипед наверняка не умеет работать с прозрачными картинками?

Честь и хвала тем программистам, которые могут удержать все в голове, и при написании подобного функционала они не забыли про прозрачность. А я вот забыл в одном из своих проектов… Хотя, чесно говоря, я взял нагугленный пример, чутка переделал для себя и встроил в проект. Но кто же знал, что клиент любит загружать прозрачные картинки…

В большинстве случаев отресайзенные картинки получали черный фон вместо прозрачного (как на картинке в заголовке статьи). Новый фон конечно может получится любого цвета, это все же зависит от самой картинки, и от ее формата. Но данная проблема известна все же как «черный фон при ресайзе прозрачных картинок».

Как всем известно, в вебе обычно используется три формата изображений: PNG, GIF, JPEG. Соответственно прозрачными могут быть только PNG и GIF. А JPEG прозрачность не поддерживает. То есть наша задача состоит в том чтобы разобраться как правильно ресайзить PNG и GIF. У этих форматов прозрачность реализованна по-разному, и обрабатывать их надо соответственно по-разному.

Ресайзим PNG

Я думаю ни для кого не секрет, что в PNG реализованна поддержка альфа канала. То есть цвет задается четырмя компонентами: R (красный), G (зеленый), B (синий), альфа. Альфа канал задает прозрачность конкретного пикселя. Впрочем это все вы должны уже знать, перейдем к сути.

Причин появления черного фона при ресайзе PNG три:

  • Программист создает не полноцветное изображение. Т.е. использует функцию imagecreate вместо imagecreatetruecolor
  • Программист забывает отключить наложение прозрачного цвета (или если правильно — «режим сопряжения цветов») с помощью функции imagealphablending. Дело в том что imagecreatetruecolor создает изображение с черным фоном. При включенном «режиме сопряжения цветов» прозрачный пиксель, не заменит фон, а будет рассчитан новый цвет в соответствии с новым цветом и цветом фона с учетом альфа канала обоих цветов — в итоге для полностью прозрачного пикселя получаем фоновой цвет — т.е. черный.
  • Программист не включает сохранение альфа канала в выходной файл. За это отвечает функция imagesavealpha

Чтож, с причинами разобрались. Теперь реализация правильного ресайза PNG картинки:

//Создаем полноцветное изображение
$destination_resource=imagecreatetruecolor($newwidth, $newheight);

//Отключаем режим сопряжения цветов
imagealphablending($destination_resource, false);

//Включаем сохранение альфа канала
imagesavealpha($destination_resource, true);

//Ресайз
imagecopyresampled($destination_resource, $source_resource, 0, 0, 0, 0, $newwidth, $newheight, $oldwidth, $oldheight);

//Сохранение
imagepng($destination_resource, $destination_path);

Ресайзим GIF

А вот GIF-е прозрачность реализованна по-другому. В нем один из используемых цветов (любой, на выбор автора картинки) объявляется как прозрачный, и при отрисовке данной картинки пиксели имеющие данный цвет заменяются прозрачными пикселями. Такая вот «псевдо-прозрачность»:
Как реализованна прозрачность в GIF
И единственная причина по которой может появится фон в прозрачной GIF — программист забывает указать «прозрачный» цвет.

В php за это отвечает функция imagecolortransparent — задает прозрачный цвет. Но сначала необходимо узнать использует ли вообще наша GIF прозрачность, это делается с помощью этой же функции. Полный алгоритм должен выглядеть приблизительно так:

//Создаем изображение, кстати для GIF можно использовать обычную imagecreate, но лучше все таки везде использовать imagecreatetruecolor
$destination_resource=imagecreatetruecolor($newwidth, $newheight);

//Получаем прозрачный цвет
$transparent_source_index=imagecolortransparent($source_resource);

//Проверяем наличие прозрачности
if($transparent_source_index!==-1){
	$transparent_color=imagecolorsforindex($source_resource, $transparent_source_index);

	//Добавляем цвет в палитру нового изображения, и устанавливаем его как прозрачный
	$transparent_destination_index=imagecolorallocate($destination_resource, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']);
	imagecolortransparent($destination_resource, $transparent_destination_index);

	//На всякий случай заливаем фон этим цветом
	imagefill($destination_resource, 0, 0, $transparent_destination_index);
}

//Ресайз
imagecopyresampled($destination_resource, $source_resource, 0, 0, 0, 0, $newwidth, $newheight, $oldwidth, $oldheight);

//Сохранение
imagegif($destination_resource, $destination_path);

Можно конечно, чуть короче и проще, но на мой взгляд так намного нагляднее.

Вывод

А мораль сей басни такова — делайте бекапы) Причем здесь бекапы? А при том… Из-за того что я не учел прозрачность при ресайзе, я запарол несколько тысяч прозрачных картинок у клиента. И слава богу что я таки сделал полный бекап еще до внесения изменений, из которого впоследствии востановил картинки обратно.

  1. mr.The

    Именно по этому стоит использовать фреймворки и либы. За пару команд можно сделать всё тоже самое даже не задумываясь, что там внутри.

      1. mr.The

        Хотя, при этом есть 1 минус. Когда нужно сделать что-то ну совсем нестандартное — приходится лезть в максимально низкий, для языка, уровень, и делать это там. Например, я одно время, вообще не писал sql, мне хватало рельсового activerecord. А потом понадобилось оптимизировать скорость пары запросов, и вот тут началось.. За-то, теперь почти специалист в этом)

  2. REZIDENT

    Спасибо автору давно искал ответ на данный вопрос какже загрузить изображение с прозрачностью не заливая фон белым или инным цветом. Спасибо еще раз очень помогла мне ваша статья.

    1. REZIDENT

      И кстате добавлю пожалуй ваш блог в закладки думаю не раз еще понадобится может чего и полезного тут найду :)

  3. progmist

    Заметил ошибку у вас в коде про то как быть с псевдо-прозрачностью в gif:

    //Проверяем наличие прозрачности
    if($transparent_source_index!==-1){
    $transparent_color=imagecolorsforindex($source_resource, $transparent_source_index);
    
    //Добавляем цвет в палитру нового изображения, и устанавливаем его как прозрачный
    $transparent_destination_index=imagecolorallocate($destination_resource, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']);
    imagecolortransparent($destination_resource, $transparent_destination_index);
    
    //На всякий случай заливаем фон этим цветом
    imagefill($destination_resource, 0, 0, $transparent_destination);
    }
    

    Где «На всякий случай заливаем фон этим цветом» переменная цвета $transparent_destination не существует
    нужно: $transparent_destination_index

  4. Андрей

    я так понимаю, скрипт ресайза гифки не распространяется на анимацию?

    1. Дмитрий Амиров Автор

      Боюсь что нет. Мне не приходилось сталкиваться с ресайзом анимированных гифок.

  5. Zyets

    Ааааа! пасибо огромное!
    Все просто и понятно! Люблю когда вот так, прям как для чайников))))

  6. Мария

    У меня тоже черный фон. Посмотрите, пожалуйста, где ошибка… Заранее, спасибо.

    function resize($target, $dest, $wmax, $hmax, $ext){
    
        /*
        $target - путь к оригинальному файлу
        $dest - путь сохранения обработанного файла
        $wmax - максимальная ширина
        $hmax - максимальная высота
        $ext - расширение файла
        */
        list($w_orig, $h_orig) = getimagesize($target);
        $ratio = $w_orig / $h_orig; // =1 - квадрат, 1 - книжная
    
        if(($wmax / $hmax) > $ratio){
            $wmax = $hmax * $ratio;
        }else{
            $hmax = $wmax / $ratio;
        }
        
        $img = "";
    
    	switch($ext){
            case(".gif"):
                $img = imagecreatefromgif($target);
                break;
            case(".png"):
                $img = imagecreatefrompng($target);
                break;
    		case(".jpeg"):
                $img = imagecreatefromjpeg($target);
                break;
    		case(".jpg"):
                $img = imagecreatefromjpeg($target);
                break;
            default:
                $img = imagecreatefromjpeg($target);    
        }  
        $newImg = imagecreatetruecolor($wmax, $hmax); // создаем оболочку для новой картинки
    	
    	//Отключаем режим сопряжения цветов
    	imagealphablending($destination_resource, false);
        
        if($ext == "png"){
            imagesavealpha($newImg, true); // сохранение альфа канала
            $transPng = imagecolorallocatealpha($newImg,0,0,0,127); // добавляем прозрачность
            imagefill($newImg, 0, 0, $transPng); // заливка  
        }
        
        imagecopyresampled($newImg, $img, 0, 0, 0, 0, $wmax, $hmax, $w_orig, $h_orig); // копируем и ресайзим изображение
        switch($ext){
            case("gif"):
                imagegif($newImg, $dest);
                break;
            case("png"):
                imagepng($newImg, $dest);
                break;
            default:
                imagejpeg($newImg, $dest);    
        }
        imagedestroy($newImg);
    }
  7. OLWerd

    Это явно не для чайников — «будет рассчитан новый цвет в соответствии с новым цветом и цветом фона с учетом альфа канала обоих цветов»

    1. Дмитрий Амиров Автор

      Честно сказать не знаю как это можно переформулировать, даже после того как вы сакцентировали на этом внимание… но вроде понятно же, нет?

  8. Евгений

    Спасибо за разъяснение, но почему-то никак не могу добиться прозрачности водяного знака при наложении его на картинку. Вот такой код:

    $picture = ImageCreateTrueColor($arDestinationSize["width"], $arDestinationSize["height"]);
    imagealphablending( $picture, false);
    imagesavealpha( $picture, true);
    $pngWaterMarkImg = @imagecreatefrompng($_SERVER["DOCUMENT_ROOT"] . $arWaterMark["path_to_watermark"]);
    imagecopyresampled($picture, $pngWaterMarkImg, ($arPictureSize["x"] - $newwidth) / 2, ($arPictureSize["y"] - $newheight) / 2, 0, 0, $newwidth, $newheight, $arWaterMarkImgSize['x'], $arWaterMarkImgSize['y']);
    imagedestroy($pngWaterMarkImg);
    

    Причем на денвере все нормально работает, а на хостинге не хочет. Подскажите, может ли что-то зависеть от настроек сервера? Может версия пхп или gd? Спасибо.

    1. Дмитрий Амиров Автор

      Боюсь что я не знаю причин такого поведения. Да и впрочем уже не особо помню особенности работы с картинками в php. Поэтому боюсь вам прийдется самому искать причину проблемы(
      PS: как решите — отпишитесь тут пожалуйста)

  9. Сереня

    юзайте imagecreatefrompng !! и памяти в 2р меньше жрет!!! качество чуть ниже.. хз. для аватарок самое то!

  10. Шум Каблионч

    А у меня в гифе, после этого преобразования остаётся только первый кадр.
    Так и должно быть?

    1. Дмитрий Амиров Автор

      Нет. Анимированные гифки должны обрабатываться немного иначе. Погуглите про ресайз анимированных гифок. Я с такой задачей не сталкивался, не смогу помочь.

  11. Alex

    if($out_extd == «gif»)
    {
    $color = imagecolorallocate($image_new, 256, 256, 256);
    /// 256 — несуществующее значение цвета. У меня работает вроде норм. Не приходится выбирать цвет «жертвоприношения». Может я что-то конечно не учитываю.
    imagecolortransparent($image_new, $color);
    imagefill($image_new, 0, 0, $color);
    }

Добавить комментарий

Прочли запись? Понравилась? Не стесняйтесь, оставьте, пожалуйста, свой комментарий. Мне очень интересно, что вы думаете об этом. Кстати в комментарии вы можете задать мне любой вопрос. Я обязательно отвечу.

Вы можете оставить коментарий анонимно, для этого можно не указывать Имя и email. Все комментарии проходят модерацию, поэтому ваш комментарий появится не сразу.