Довольно давно работаю в веб-студии, занимаемся разработкой сайтов в back-end'ом на php. Практически на каждом проекте используется «универсальный» ужиматель картинок, основными целями которого являются уменьшение объема загружаемых данных и избавление вносящего контент от мыслей о правильных пропорциях изображений.

Постепенно скрипт модернизировался, на данный момент постарался сделать его максимально удобным и быстрым в подключении к любому сайту. Основные возможности:

— ресайз jpg, png, gif;
— возможность задания ширины и высоты сжатого изображения либо только одного из параметров, второй будет подсчитан пропорционально от исходного изображения;
— три вида ресайзинга (добавление полос — определенного цвета или прозрачных, обрезка изображения);
— кеширование обработанных картинок.

Примеры и код под катом.

Для работы ресайзера используется собственно сам скрипт и файл .htaccess для более простого задания адресов картинок. В принципе, можно обойтись и без последнего, но тогда атрибут src у изображений будет несколько страшным.

В случае использования htaccess вызов ресайзера выглядит следующим образом:
image/<resize_type>/<new_width>/[new_height]/<img_src>
Где:
  • resize_type — обязательный параметр, принимает значения b — borders, t — transparent, c — crop;
  • new_width — обязательный параметр, ширина нового изображения в пикселах. Если указан 0 — посчитает ширину изображения автоматически, исходя из высоты нового изображения с сохранением пропорций. В этом случае, поскольку полученная картинка будет полностью пропорциональна исходной выбор вида ресайзинга не играет никакой роли. Использование нуля актуально для горизонтальных слайдеров, высота которых должна быть фиксирована;
  • new_height — необязательный параметр высоты нового изображения. Если его не указать или указать 0 — посчитает автоматически с сохранением пропорций;
  • img_src — путь к картинке.

Собственно, примеры работы в различных режимах с одним и тем же исходным изображением. Размер исходной картинки — 450х411 пикселей.

Ресайз с добавлением полос к размеру 300х200 (image/b/300/200/img_1.jpg):



Работа ресайзера в данном случае простая. Сначала определяется, какая из сторон имеет больший коэффициент сжатия: 450/300 = 1.5 и 411/200 = 2.055. Затем изображение уменьшается в 2.055 раза по обоим параметрам, в результате чего получаем картинку в размере 219х200 и добавляются полосы слева и справа до заданных 300 пикселей. Аналогичным образом будет выглядеть и использование параметра t, так как формат сохраняемого в кеше изображения совпадает с исходным, а правильное его отображение (с прозрачностью) достигается за счет передачи header'ов для png изображений.

Ресайз с пропорциональным обрезанием к размеру 300х200 (image/c/300/200/img_1.jpg):



В данном случае будет выбран минимальный из коэффициентов сжатия, в данном случае — 1.5. Затем изображение уменьшается в 1.5 раза по обоим параметрам, что дает на выходе 300х274, а затем лишние 74 будут обрезаны — по 37 сверху и снизу.

Если не указать параметр высоты, оставив только ширину, то она будет рассчитана автоматически. К примеру, для ширины 300 — это 2/3 от ширины исходного изображения, значит и новая высота должна составить 2/3 от исходных 411, а конкретно 274 (image/b/300/img_1.jpg):



Аналогичным образом, если указать ширину равной нулю, то получим её автоматический расчет (image/b/0/300/img_1.jpg):



Сам скрипт
image.php
<?

error_reporting( 0 );
//цвет "полос" по бокам
$color = array( 255, 255, 255 );
$file_name = $_GET["src"];
list( $width, $height, $ext ) = getimagesize( $file_name );

//задаем новые ширину и высоту
$params["width"] = (int)$_GET["w"];
if( !isset( $_GET["h"] ) || 0 == (int)$_GET["h"] ){
	$params["height"] = $height * $params["width"]/$width;
} else {
	$params["height"] = (int)$_GET["h"];
}
if( 0 == $params["width"] ){
	$params["width"] = $width * $params["height"]/$height;
}

//папка с кешем изображений. Внутри img_cache/ повторяется полный путь исходного изображения
$cache_file_folder = "img_cache/" . substr( $file_name, 0, strrpos( $file_name, "/" ) );
$cache_file_name = "img_cache/" . substr( $file_name, 0, strrpos( $file_name, "." ) ) . "_" . $_GET["resize_type"] . "_" . $params["width"] . "_" . $params["height"] . substr( $file_name, strrpos( $file_name, "." ) );

//проверка наличия изображения в кеше. Если оно есть и было создано после последнего изменения исходного изображения - берем его
if( file_exists( $cache_file_name ) ){
	if( filemtime( $cache_file_name ) > filemtime( $file_name ) ){
		if( 1 == $ext ){
			header( "Content-type: " . image_type_to_mime_type( IMAGETYPE_GIF ) );
		} else if( 2 == $ext ){
			header( "Content-type: " . image_type_to_mime_type( IMAGETYPE_JPEG ) );
		} else if( 3 == $ext ){
			header( "Content-type: " . image_type_to_mime_type( IMAGETYPE_PNG ) );
		}
		//обновление времени последнего изменения. Используется, если необходима очистка давно не запрашиваемых картинок
		touch( $cache_file_name );
		echo file_get_contents( $cache_file_name );
		exit();
	}
}

//пытаемся создать папку для кеша
mkdir( $cache_file_folder, 0755, true );

//непосредственно код ресайзинга
switch( $_GET["resize_type"] ){
	case "b":
		if( 1 == $ext ){
			header( "Content-type: " . image_type_to_mime_type( IMAGETYPE_GIF ) );
			$image = imagecreatefromgif( $file_name );
		} else if( 2 == $ext ){
			header( "Content-type: " . image_type_to_mime_type( IMAGETYPE_JPEG ) );
			$image = imagecreatefromjpeg( $file_name );
		} else if( 3 == $ext ){
			header( "Content-type: " . image_type_to_mime_type( IMAGETYPE_PNG ) );
			$image = imagecreatefrompng( $file_name );
		}
		$new_width = (int)$params["width"];
		$new_height = (int)$params["height"];
		if( $new_width == 0 || $new_height == 0 || !$image || !in_array( $ext, array( 1, 2, 3 ) ) ){
			$image_new = imagecreatetruecolor( 100, 30 );
			$bgc = imagecolorallocatealpha( $image_new, $color[0], $color[1], $color[2], 127 );
			$tc = imagecolorallocate( $image_new, 0, 0, 0 );
			imagefilledrectangle( $image_new, 0, 0, 100, 30, $bgc );
			imagestring( $image_new, 1, 5, 5, "Error loading", $tc );
			if( 1 == $ext ){
				imagegif( $image_new );
				imagedestroy( $image_new );
			} else if( 2 == $ext ){
				imagejpeg( $image_new );
				imagedestroy( $image_new );
			} else if( 3 == $ext ){
				imagepng( $image_new );
				imagedestroy( $image_new );
			}
			exit();
		}
		$image_new = imagecreatetruecolor( $new_width, $new_height );
		imagealphablending( $image_new, false );
		imagesavealpha( $image_new, true );
		$bgc = imagecolorallocatealpha( $image_new, $color[0], $color[1], $color[2], 127 );
		imagefilledrectangle( $image_new, 0, 0, $new_width, $new_height, $bgc );
		if( $new_width/$new_height > $width/$height ){
			$ins = $width*($new_height/$height);
			$out = ( $new_width - $ins )/2;
			imagecopyresampled( $image_new, $image, $out, 0, 0, 0, $ins, $new_height, $width, $height );
		} else {
			$ins = $height*($new_width/$width);
			$out = ( $new_height - $ins )/2;
			imagecopyresampled( $image_new, $image, 0, $out, 0, 0, $new_width, $ins, $width, $height );
		}
		
		if( 1 == $ext ){
			imagegif( $image_new );
			imagegif( $image_new, $cache_file_name );
			imagedestroy( $image_new );
		} else if( 2 == $ext ){
			imagejpeg( $image_new );
			imagejpeg( $image_new, $cache_file_name );
			imagedestroy( $image_new );
		} else if( 3 == $ext ){
			imagepng( $image_new );
			imagepng( $image_new, $cache_file_name );
			imagedestroy( $image_new );
		}
	break;
	case "t":
		if( 1 == $ext ){
			$image = imagecreatefromgif( $file_name );
		} else if( 2 == $ext ){
			$image = imagecreatefromjpeg( $file_name );
		} else if( 3 == $ext ){
			$image = imagecreatefrompng( $file_name );
		}
		header( "Content-type: " . image_type_to_mime_type( IMAGETYPE_PNG ) );
		$new_width = (int)$params["width"];
		$new_height = (int)$params["height"];
		if( $new_width == 0 || $new_height == 0 || !$image || !in_array( $ext, array( 1, 2, 3 ) ) ){
			$image_new = imagecreatetruecolor( 100, 30 );
			$bgc = imagecolorallocatealpha( $image_new, $color[0], $color[1], $color[2], 127 );
			$tc = imagecolorallocate( $image_new, 0, 0, 0 );
			imagefilledrectangle( $image_new, 0, 0, 100, 30, $bgc );
			imagestring( $image_new, 1, 5, 5, "Error loading", $tc );
			if( 1 == $ext ){
				imagegif( $image_new );
				imagedestroy( $image_new );
			} else if( 2 == $ext ){
				imagejpeg( $image_new );
				imagedestroy( $image_new );
			} else if( 3 == $ext ){
				imagepng( $image_new );
				imagedestroy( $image_new );
			}
			exit();
		}
		$image_new = imagecreatetruecolor( $new_width, $new_height );
		imagealphablending( $image_new, false );
		imagesavealpha( $image_new, true );
		$bgc = imagecolorallocatealpha( $image_new, $color[0], $color[1], $color[2], 127 );
		imagefilledrectangle( $image_new, 0, 0, $new_width, $new_height, $bgc );
		if( $new_width/$new_height > $width/$height ){
			$ins = $width*($new_height/$height);
			$out = ( $new_width - $ins )/2;
			imagecopyresampled( $image_new, $image, $out, 0, 0, 0, $ins, $new_height, $width, $height );
		} else {
			$ins = $height*($new_width/$width);
			$out = ( $new_height - $ins )/2;
			imagecopyresampled( $image_new, $image, 0, $out, 0, 0, $new_width, $ins, $width, $height );
		}
		imagepng( $image_new );
		imagepng( $image_new, $cache_file_name );
		imagedestroy( $image_new );
	break;
	case "c":
		if( 1 == $ext ){
			header( "Content-type: " . image_type_to_mime_type( IMAGETYPE_GIF ) );
			$image = imagecreatefromgif( $file_name );
		} else if( 2 == $ext ){
			header( "Content-type: " . image_type_to_mime_type( IMAGETYPE_JPEG ) );
			$image = imagecreatefromjpeg( $file_name );
		} else if( 3 == $ext ){
			header( "Content-type: " . image_type_to_mime_type( IMAGETYPE_PNG ) );
			$image = imagecreatefrompng( $file_name );
		}
		$new_width = (int)$params["width"];
		$new_height = (int)$params["height"];
		if( $new_width == 0 || $new_height == 0 || !$image || !in_array( $ext, array( 1, 2, 3 ) ) ){
			$image_new = imagecreatetruecolor( 100, 30 );
			$bgc = imagecolorallocate( $image_new, 255, 255, 255 );
			$tc = imagecolorallocate( $image_new, 0, 0, 0 );
			imagefilledrectangle( $image_new, 0, 0, 100, 30, $bgc );
			imagestring( $image_new, 1, 5, 5, "Error loading", $tc );
			if( 1 == $ext ){
				imagegif( $image_new );
				imagedestroy( $image_new );
			} else if( 2 == $ext ){
				imagejpeg( $image_new );
				imagedestroy( $image_new );
			} else if( 3 == $ext ){
				imagepng( $image_new );
				imagedestroy( $image_new );
			}
			exit();
		}
		$image_new = imagecreatetruecolor( $new_width, $new_height );
		imagealphablending( $image_new, false );
		imagesavealpha( $image_new, true );
		$width/$new_width < $height/$new_height ? $coef = $width/$new_width : $coef = $height/$new_height;
		$start_x = ( $width - $new_width*$coef )/2;
		$start_y = ( $height - $new_height*$coef )/2;
		$end_x = $width - 2*$start_x;
		$end_y = $height - 2*$start_y;
		imagecopyresampled( $image_new, $image, 0, 0, $start_x, $start_y, $new_width, $new_height, $end_x, $end_y );
		if( 1 == $ext ){
			imagegif( $image_new );
			imagegif( $image_new, $cache_file_name );
			imagedestroy( $image_new );
		} else if( 2 == $ext ){
			imagejpeg( $image_new );
			imagejpeg( $image_new, $cache_file_name );
			imagedestroy( $image_new );
		} else if( 3 == $ext ){
			imagepng( $image_new );
			imagepng( $image_new, $cache_file_name );
			imagedestroy( $image_new );
		}
	break;
}

.htaccess:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^image/([a-z])/([0-9]+)/([0-9]+)/([A-z0-9-\/\.]+)$ image.php?resize_type=$1&w=$2&h=$3&src=$4 [QSA,L]

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^image/([a-z])/([0-9]+)/([A-z0-9-\/\.]+)$ image.php?resize_type=$1&w=$2&src=$4 [QSA,L]

</IfModule>



Спасибо за внимание. Надеюсь, скрипт кому-нибудь да пригодится.

Комментарии ()


  1. iSage
    12.05.2015 11:38
    +6

    oh. god. why.


  1. IncorrecTSW
    12.05.2015 11:39
    +3

    Каждый раз, когда кто то пишет такие свичи (код), в мире умирает котенок.


  1. maximw
    12.05.2015 11:43
    -2

    Без белого списка размеров вам такую DoS устроят, плюс забьют все дисковое пространство.


  1. markoffko
    12.05.2015 11:45
    +1

    Интересно в какой студии вы работаете.


  1. mkuzmin
    12.05.2015 11:47
    -1

    github.com/thumbor/thumbor
    thumbor is an open-source photo thumbnail service by globo.com


  1. maxru
    12.05.2015 12:24

    1. SerafimArts
      12.05.2015 12:29

      В добавку: http://image.intervention.io/getting_started/installation


  1. maxru
    12.05.2015 12:26
    +12

    image


  1. nikitasius
    12.05.2015 12:43
    -2

    • К чему все это, когда есть convert?

    Если ложно то

    • К чему хостинг, где нету convert?


  1. sashabeep
    12.05.2015 14:15

    TimThumb тоже норм


    1. zelenin
      12.05.2015 18:54

      Tim «Бесконечная дыра» Thumb


      1. sashabeep
        12.05.2015 18:55

        Ну или phpthumb


        1. zelenin
          12.05.2015 19:01

          а какая разница? Это два одинаковых поделия, в которых регулярно находят дыры, и благодаря которым постоянно заражаются сайты.


          1. sashabeep
            12.05.2015 19:25

            dj всех системах регулярно находят дыры в-общем. Приведенное решение гораздо более дыряво при беглом взгляде


  1. jonic
    12.05.2015 15:24
    +1

    а у меня nginx на лету жмет и кэшируется cdn :)


  1. WindDrop
    12.05.2015 21:06
    +1

    Вот раз — http://habrahabr.ru/post/94435/, вот два — http://habrahabr.ru/post/245165/.
    И забудьте про такие скрипты.


  1. newkamikaze
    13.05.2015 21:31

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