Сайт Романа ПарпалакаЗаметкиТехнологииВеб-разработкаМиниатюры на PHP

Миниатюры на PHP

21 октября 2007 года

Статья написана для интернет-издания Hostinfo.ru.

В ходе разработки и обслуживания сайтов часто возникает необходимость в создании миниатюр — уменьшенных копий изображений. В тексте страницы вместо большой картинки можно поместить такую копию, являющуюся ссылкой на исходный файл. Работа фотоальбомов и галерей изображений, менеджеров файлов в CMS немыслима без автоматического создания миниатюр. Мы разберем скрипт, генерирующий подобные миниатюры, и на его примере познакомимся с некоторыми возможностями PHP для работы с изображениями.

PHP обрабатывает изображения с помощью библиотеки GD. Почти на всех хостинговых площадках эта библиотека установлена. Если вдруг вам не повезло, обратитесь в техническую поддержку вашего хостинга. В нашем скрипте будем использовать функции, появившиеся в библиотеке GD версии 2.0.1.

Сначала определим требования, которым будет отвечать генератор миниатюр. Он должен создавать уменьшенные копии изображений, сохраненных в форматах GIF, JPEG и PNG. Если исходное изображение обладало прозрачностью, то и миниатюра должна сохранять прозрачность. Также не следует пренебрегать безопасностью и быстродействием.

Мы определились с тем, чего хотим. Теперь можно приступать к программированию. Исходный файл и размеры конечной картинки будем передавать через URL:

$width = isset($_GET['width']) ? (int) $_GET['width'] : 0;
$height = isset($_GET['height']) ? (int) $_GET['height'] : 0;
$max_size = isset($_GET['max_size']) ? (int) $_GET['max_size'] : 0;
$file_name = $_GET['file'];

Для указания размера миниатюры предусмотрим два способа. Можно задать параметр max_size, тогда размеры изображения на выходе будут пропорциональны исходным, но изменятся так, что его можно будет вписать в квадрат размером max_size*max_size. Можно также задавать размер изображения на выходе напрямую в параметрах width и height (при отсутствии одного из них картинка будет сжата пропорционально).

Договоримся, что скрипт будет генерировать миниатюры для файлов, расположенных в корневой директории веб-сервера или в поддиректориях. Для предотвращения несанкционированного доступа к другим файлам условимся, что параметр file в URL задает путь к файлу от корневой директории и начинается со знака '/'.

$file_name = str_replace('..', '', $file_name);
$file_name = $_SERVER['DOCUMENT_ROOT'] . $file_name;

if (!is_file($file_name)) {
    echo 'Ошибка: файл не найден';
    exit();
}

Рассмотрим функцию, которая будет выполнять основную задачу скрипта.

function make_thumbnail($file_name, $thumb_width, $thumb_height,
    $max_size) {

Для начала получим информацию об исходном изображении:

    $image_info = getimagesize($file_name);

Функция getimagesize возвращает массив $image_info, содержащий значения некоторых характеристик изображения:

В зависимости от mime-типа мы будем вызывать свою функцию для открытия и загрузки изображения: imagecreatefromGIF, imagecreatefromJPEG или imagecreatefromPNG.

    switch ($image_info['mime']) {
        case 'image/gif':
            if (imagetypes() & IMG_GIF) {
                $image = imagecreatefromGIF($file_name);
            }
            else {
                $err_str = 'GD не поддерживает GIF';
            }
            break;
        case 'image/jpeg':
            if (imagetypes() & IMG_JPG) {
                $image = imagecreatefromJPEG($file_name);
            }
            else {
                $err_str = 'GD не поддерживает JPEG';
            }
            break;
        case 'image/png':
            if (imagetypes() & IMG_PNG) {
                $image = imagecreatefromPNG($file_name);
            }
            else {
                $err_str = 'GD не поддерживает PNG';
            }
            break;
        default:
           $err_str = 'GD не поддерживает ' . $image_info['mime'];
    }

    if (isset($err_str)) {
        return $err_str;
    }

Теперь нужно определить размеры миниатюры, если они не заданы явно.

    $image_width = imagesx($image);
    $image_height = imagesy($image);

    //задано ограничение на высоту и ширину:
    if ($max_size) {
        if ($image_width < $image_height) {
            $thumb_height = $max_size;
            $thumb_width =
                round($max_size * $image_width / $image_height);
        }
        else {
            $thumb_width = $max_size;
            $thumb_height =
                round($max_size * $image_height / $image_width);
        }
    }

    //задана только ширина
    elseif ($thumb_width && !$thumb_height) {
        $thumb_height =
            round($thumb_width * $image_height / $image_width);
    }

    //задана только высота
    elseif (!$thumb_width && $thumb_height) {
        $thumb_width =
            round($thumb_height * $image_width / $image_height);
    }

    //не задан ни один из размеров
    else {
        $thumb_width = $image_width;
        $thumb_height = $image_height;
    }

Функция imagecreatetruecolor создает полноцветное изображение указанного в параметрах размера и возвращает его идентификатор. imagealphablending позволяет выбрать один из двух способов работы с альфа-каналом. Если второй параметр этой функции установлен в true, то при рисовании полупрозрачным цветом новое изображение частично накладывается на старое. Если же он установлен в false, то происходит полное замещение пикселей исходного изображения на пиксели нового. Ясно, что нам нужен именно второй режим. Функция imagesavealpha необходима для того, чтобы при вызове функции imagePNG альфа-канал был сохранен в результирующее изображение.

    $thumb = imagecreatetruecolor($thumb_width, $thumb_height);
    imagealphablending($thumb, false);
    imagesavealpha($thumb, true);

Теперь настало время вызвать функцию, которая и выполнит основную задачу этого скрипта. imagecopyresampled копирует прямоугольную область из первого изображения во второе. Она принимает на вход идентификаторы изображений, координаты верхнего левого угла области во втором изображении, координаты верхнего левого угла области в первом изображении, ширину и высоту области во втором изображении и ширину и высоту области во первом. Если размеры областей различны, копируемое изображение растягивается или сжимается. При этом применяется сглаживание, так что качество картинки остается удовлетворительным.

Следует отметить, что есть и другая функция, imagecopyresized, которая похожа на imagecopyresampled. Качество картинки, созданной imagecopyresized, трудно признать удовлетворительным, поскольку эта функция не производит сглаживание, но она работает быстрее. Поэтому в некоторых ситуациях вполне допустимо ее использовать.

    imagecopyresampled($thumb, $image, 0, 0, 0, 0,
        $thumb_width, $thumb_height, $image_width, $image_height);

Так как мы хотим сохранить прозрачность исходного изображения, миниатюру приходится сохранять в формате PNG. Функция imagePNG выводит в браузер изображение с указанным идентификатором.

    imagePNG($thumb);

    //освобождаем память
    imagedestroy($image);
    imagedestroy($thumb);
}

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

//промежуточный вариант!
header('Content-Type: image/png');
make_thumbnail($file_name, $width, $height, $max_size);

мы получим работоспособный скрипт.

Следует отметить, что функция header устанавливает заголовки ответа сервера, и в данном случае она указывает на тип (картинка PNG) возвращаемых данных. Заголовок Content-Type, как и любые заголовки, необходимо устанавливать до операций вывода. Однако при работе с графикой устанавливать этот заголовок нужно как можно ниже, так как сообщения об ошибках, появляющихся после установки Content-Type: image/png не будут выводиться в браузер.

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

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

Рассмотрим, как можно сделать кеширование миниатюр. Мы будем сохранять их в файлы с именами, являющимися md5-хешами имен файлов с исходными изображениями, например, в папку img_cache в корневой директории веб-сервера.

define ('IMG_CACHE', $_SERVER['DOCUMENT_ROOT'].'/img_cache/');

//для корректной работы filemtime
clearstatcache();

//имя файла с кешем
$cache_file_name = md5($file_name);

Определим время изменения файла с кешем, если он существует.



$cache_mtime = 0;
if (is_file(IMG_CACHE . $cache_file_name)) {
    $cache_mtime = filemtime(IMG_CACHE . $cache_file_name);
}

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

Наша функция make_thumbnail сразу отправляет миниатюру браузеру. Однако предварительно нужно сохранить миниатюру в кеш. Поэтому мы не должны вызывать функцию make_thumbnail непосредственно, так, как сделали это выше. Нужно перехватить вывод с помощью буферизации. После выполнения участка кода между ob_start и ob_end_clean в переменной $thumbnail будут содержаться те данные, которые функция imagePNG собиралась отправить браузеру, а в переменной $thumb_size — размер данных.

if ($cache_mtime < filemtime($file_name)) {

    //буферизация вывода
    ob_start();
    $result = make_thumbnail($file_name, $width, $height, $max_size);
    $thumbnail = ob_get_contents();
    $thumb_size = ob_get_length();
    ob_end_clean();
    if ($result) {
        echo 'Ошибка: ' . $result;
        exit();
    }

    //кеширование миниатюры
    $fd = fopen(IMG_CACHE . $cache_file_name, "wb");
    fwrite($fd, $thumbnail);
    fclose($fd);

    $cache_mtime = filemtime(IMG_CACHE . $cache_file_name);
}
else {
    //загрузка миниатюры из кеша
    $fd = fopen(IMG_CACHE . $cache_file_name, "rb");
    $thumb_size = filesize(IMG_CACHE . $cache_file_name);
    $thumbnail = fread ($fd, $thumb_size);
    fclose ($fd);
}

Стоит отметить, что в директории img_cache будут оставаться уменьшенные копии удаленных картинок. Поэтому при масштабном изменении структуры сайта кеш нужно очищать вручную.

Осталось установить заголовки и отправить миниатюру браузеру.

header('Content-Type: image/png');
//время создания миниатюры
header('Last-Modified: '.gmdate('D, d M Y H:i:s', $cache_mtime).' GMT');
header('Content-Length: '.$thumb_size);

//вывод миниатюры в браузер
echo $thumbnail;

Теперь нужно собрать участки кода в один файл, например, preview.php (разместив функцию make_thumbnail в начале) и поместить его в корневую директорию веб-сервера. Там же нужно создать директорию img_cache и установить права доступа к ней в 777. Всё готово!

Пусть изображение pict.jpg лежит в директории images, то есть его можно вставить на страницу с помощью кода <img src="/images/pict.jpg" ... >. Тогда код, помещающий миниатюру, примет вид <img src="/preview.php?file=/images/pict.jpg&max_size=100" ... >.

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

Поделиться
Посмотрите в блоге

Комментарии

#1. 27 ноября 2007 года, 09:20. пишет:
Роман, библиотека GD сосёт.
На одном сайте после перехода на ImageMagick количество загружаемых фотографий выросло на 30%. Это как раз те случаи, когда GD падал с ошибками. К тому же качество масштабирования у него оставляет желать лучшего.
В то время как я это делал модуль ImageMagic для PHP был ущербным, поэтому вызывали через командную строку.

$options = '';
$options .= « -crop {$p['crop']['w']}x{$p['crop']['h']}+{$p['crop']['x']}+{$p['crop']['y']}»;
$options .= « -filter Lanczos -thumbnail {$p['w']}x{$p['h']}!»;

$shell_src_file = escapeshellcmd($p['src_file']);
$shell_dst_file = escapeshellcmd($p['dst_file']);
$command = «convert '{$shell_src_file}[0]' {$options} -quality 80 '{$shell_dst_file}'»;
$is_saved = !exec($command);
#2. 27 ноября 2007 года, 15:03. пишет:
Может быть, всё зависит от версии библиотеки GD?

Я использую описанный мной подход в менеджере картинок в CMS собственного изготовления. Никаких проблем при работе с ним у меня не возникало.

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

Функция imagecopyresampled() создает картинки нового размера вполне приемлемого качества. На изображении никаких артефактов не заметно.
#3. 6 декабря 2007 года, 15:18. пишет:
Вы знаете, я всю голову поломал, но у меня так и не хочет работать данный прием.
#4. 6 декабря 2007 года, 17:42. пишет:
Я сейчас просто скопировал все приведенные фрагменты кода (за исключением одного, для которого явно указано, что он из промежуточной версии) в файл и проследовал всем остальным инструкциям, приведенным в конце статьи. У меня всё сразу заработало, в тексте ошибок нет.

Вообще, трудно что-либо сказать без более подробной информации о том, что именно не работает.

Убедитесь, что по адресу [домен]/images/pict.jpg (или какой-нибудь другой путь) в браузере открывается исходная картинка. Потом попробуйте ввести такой адрес: [домен]/preview.php?file=/images/pict.jpg&max_size=100. Если миниатюра не будет выведена, закомментируйте временно заголовок
header('Content-Type: image/png');
и посмотрите на сообщения об ошибках.
#5. 18 мая 2008 года, 17:49. Иван пишет:
спасибо, реально очень помогло
#6. 28 февраля 2009 года, 19:47. пишет:
Спасибо, то что было мне нужно!

Но есть одна проблема оригинальный файл размером 800х533 и «весом» 282 кб., после сжатия его до размеров 600х400 начинает «весить» — ! 360 кб. ((( Как избежать этого?
#7. 28 февраля 2009 года, 19:52. пишет:
Ааа, я понял! Размер увеличивается из-за перевода его в формат PNG! Но почему PNG ведь оригинальный файл JPG?
#8. 28 февраля 2009 года, 20:23. пишет:
Потому что PNG поддерживает альфа-канал, а исходные изображения могут быть прозрачными. Конечно, правильнее будет не менять вид сжатия, если исходное сжатие было JPEG. В простейшем случае нужно функцию imagePNG заменить на imageJPEG и убрать вызовы

imagealphablending($thumb, false);
imagesavealpha($thumb, true);
#9. 28 декабря 2009 года, 11:45. Серг пишет:
<?php
$file_name = str_replace('..', '', $file_name);
$file_name = $_SERVER['DOCUMENT_ROOT'] . $file_name;
if (!is_file($file_name)) {
echo 'Ошибка: файл не найден';
exit();
}
?>

а если в имени файла 2 точки подряд (например te..st..jpg)? их же вырезает и такого файла не находит (получается имя файла в переменной testjpg, а сам файл не переименовывается).
#10. 28 декабря 2009 года, 20:36. пишет:
Согласитесь, две точки подряд в имени файла — вещь достаточно необычная.

Если вы хотите обрабатывать такие имена файлов, замените str_replace('..', ", $file_name) на что-то более адекватное, запрещающее подъем вверх по структуре директорий ('../').
#11. 11 октября 2010 года, 10:45. пишет:
Большое спасибо все работает зашибись ))
вот можно глянуть тут :) http://victoria-k.dp.ua/photo/wedding/
#12. 26 октября 2010 года, 07:12. пишет:
Спасибо, Ваша статья помогла разобраться с функцией.
#13. 24 сентября 2011 года, 21:31. Freewww пишет:
Пример создания миниатюр изображений на PHP...
http://magibagi.kz/browse-post.php?post=139
#14. 5 апреля 2012 года, 22:01. Ruslan пишет:
Весь день, вечер рыл инет, только у Вас уже к ночи нашол реально полезное, работающее а главное простое и единственно правильное решение.

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

Я уже даже подумывал использовать вместо GD создания фонового слоя, прозрачную 1 пиксельную картинку, увеличивать ее до нужного размера и совмещать с источником.

Да и по вопросу так сказать кеша «динамических картинок» — идея неплохая. Я думал сделать что-то похожее. Но почитав Ваши соображения сделаю не так, как хотел и нет как у Вас, лучше. Но опять таки после прочтения этой статьи.

Спасибо огромное.
#15. 7 ноября 2012 года, 10:54. Guga пишет:
Good article

Оставьте свой комментарий

Ваше имя:

Комментарий:

Для выделения используйте следующий код: [i]курсив[/i], [b]жирный[/b].
Цитату оформляйте так: [q = имя автора]цитата[/q] или [q]еще цитата[/q].
Ссылку начните с http://. Других команд или HTML-тегов здесь нет.

Сколько будет 62+8?