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


Мы решили написать универсальный скрипт, который можно запустить из консоли или прямо в браузере и обновить все картинки на сайте.


И так задача:


  1. Картинки находятся в папку img в корне сайта;
  2. Добавить на картинки водяной знак по центру;
  3. Перенести все картинки в папку img2;

В процессе решения задачи, оказалось, что все картинки ещё и разного размера от 200 до 7000 пикселей по ширине, а водный знак в виде логотипа должен быть на всех.Как мы решали эту задачу:


Этап 1. Обойти все файлы


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


$path  =  $_SERVER['DOCUMENT_ROOT']; //Переменная окружения
$root  =  $path."/img"; //Рабочая папка

И функция обхода всех элементов папки:


function find_new($dir)
{
    $new_dir = null;
    $dir_files = opendir($dir);
    while(false !== ($file = readdir($dir_files)))
    {

        if($file != '.' && $file != '..')
            $new_dir[] = $dir."/".$file;
    }

    if($new_dir)
        foreach($new_dir as $check )
        {
            if(is_file($check)) {
                echo $check . "<br>";
            } elseif(is_dir($check))
                find_new($check);
        }
}
find_new($root);

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


Единственное что тут не реализована, проверка на картинки, но она нам не пригодилась.


Этап 2. Воссоздать структуру папок


Так как у нас множество папок, с вложенными папками, и так до 10 уровня, для успешной работы всех функций копирования и перемещения файлов, нужна готовая структура.


Для этого мы определяем имя конечно папки и если её нет, то создаем:


$fileName = basename($check); //Определяем имя файла
$new = str_replace("img","img2",$check); //Заменяем имя папки
$put = substr($new,0,-strlen($fileName)); //Определяем путь до папки
if (!file_exists($put)) {
    mkdir($put, 0777, true); //Если папки нет, то создаем
}

Данный код, вставляется после: echo $check; и при выполнении он генерирует новую структуру папок, на вашем сервере, при этом, запускать его можно бесконечно много раз, он не повредит структуру, а создает соседнею папку img2.


Этап 3. Добавить логотип на картинки


Для этого будем использовать четыре стандартные функции: imagecreatefrompng, imagecreatefromjpeg, imagecopy, imagejpeg и парочку дополнительных типа: imagedestroy, imagesx.


Все это библиотека GD для PHP, подключена у всех по умолчанию и так:


$stamp = imagecreatefrompng('stamp.png'); //Логотип, размер произвольный
$sx = imagesx($stamp); //Получаем ширину
$sy = imagesy($stamp); //Высоту
$im = imagecreatefromjpeg($check); //Исходная картинка
imagecopy($im, $stamp, imagesx($im) - $sx - 10,  imagesy($im) - $sy - 10, 0, 0, imagesx($stamp), imagesy($stamp)); //Копируем логотип на картинку
imagejpeg($im, $new, 100); //Создаем изображение
imagedestroy($im); //Чистим за собой

Таким образом, после 3-5 минут работы скрипта, в зависимости от количества файлов, у нас получается копия всех изображений в папке img2, но логотип находится в правом нижнем углу, и на всех картинках имеет разный размер. Можно поиграться с цифрами в imagecopy но эффекта от этого не будет. Картинки разные, значит и водный знак должен быть разным, поэтому мы переходим к 4 этапу.


4 этап. Адаптация и выравнивание логотипа


Для этого нам понадобиться преобразовать, исходный логотип stamp.png до размеров картинки, на которой будем его размещать, и вклеить точно по её середине.
Приступим:


$stamp = imagecreatefrompng('stamp.png'); //Логотип, размер БОЛЬШОЙ желательно 2000*1500
$sx = imagesx($stamp); //Получаем ширину
$sy = imagesy($stamp); //Высоту
$im = imagecreatefromjpeg($check); //Загружаем картинку
///Подгоняем знак по размерам
$w = imagesx($im) - 20; //Новый размер логотипа ширина
$koe=$sx/$w; //Определяем коэффициент сжатия
$h=ceil($sy/$koe); //Высота нового логотипа
//echo $sx."-".$sy." ".$w."-".$h." ".$koe."<BR>"; //Это для теста
$sim = imagecreatetruecolor($w, $h); //Создаем новый фон для логотипа
$transparent = imagecolorallocatealpha($sim, 0, 0, 0, 127); // для прозрачности фона
imagefill($sim, 0, 0, $transparent); // для прозрачности фона
imagesavealpha($sim, true); // для прозрачности фона
imagecopyresampled($sim,$stamp,0,0,0,0,$w,$h,$sx,$sy); //Создали новый логотип в $sim
$cn = ceil((imagesy($im) - $h)/2); //Установим по центру водяной знак
imagecopy($im, $sim, imagesx($im) - $w - 10,  $cn, 0, 0, imagesx($sim), imagesy($sim)); //Вставляем штамп на картинку

Если на этапе 3, в imagecopy в качестве размеров вставляемой картинки использовали imagesx($stamp), то здесь уже используем размеры нового логотипа imagesx($sim).


Логотип содержит отступы 10 пикселей слева и справа, и задается соответственно цифрами 20 и 10 в коде.


Этап 5. Добавляем функцию преобразования в наш цикл:


Так как скрипт собирался на коленке, естественно его можно ещё упростить и улучшить, ваши предложения в комментариях. Но вот уже рабочий вариант:


path  =  $_SERVER['DOCUMENT_ROOT'];
$root  =  $path."/img";

$stamp = imagecreatefrompng('stamp.png');
$sx = imagesx($stamp);
$sy = imagesy($stamp);

function find_new($dir)
{
    global  $stamp;
    global  $sx;
    global  $sy;

    $new_dir = null;
    $dir_files = opendir($dir);
    while(false !== ($file = readdir($dir_files)))
    {

        if($file != '.' && $file != '..')
            $new_dir[] = $dir."/".$file;
    }

    if($new_dir)
        foreach($new_dir as $check )
        {
            if(is_file($check)) {
                $w='';$h='';$koe='';$sim='';

                //echo $check . "<br>";
                $im = imagecreatefromjpeg($check);

                ///Подгоняем знак по размерам
                $w = imagesx($im) - 20; //Новый размер штампа ширина
                $koe=$sx/$w;
                $h=ceil($sy/$koe); //высота его
                //echo $sx."-".$sy." ".$w."-".$h." ".$koe."<BR>";
                $sim = imagecreatetruecolor($w, $h);
                $transparent = imagecolorallocatealpha($sim, 0, 0, 0, 127); // для прозрачности фона
                imagefill($sim, 0, 0, $transparent); // для прозрачности фона
                imagesavealpha($sim, true); // для прозрачности фона
                imagecopyresampled($sim,$stamp,0,0,0,0,$w,$h,$sx,$sy);

                $cn = ceil((imagesy($im) - $h)/2); //Установим по центру водяной знак

                //Вставляем штамп на картинку
                imagecopy($im, $sim, imagesx($im) - $w - 10,  $cn, 0, 0, imagesx($sim), imagesy($sim));
                $fileName = basename($check);
                $put = substr($check,0,-strlen($fileName));
                $put = str_replace("img","cache",$put);
                if (!file_exists($put)) {
                    mkdir($put, 0777, true);
                }
                $new = str_replace("img","cache",$check);

                imagejpeg($im, $new, 100); 
                imagedestroy($im);

            } elseif(is_dir($check))
                find_new($check);
        }
}
find_new($root);

Его достаточно положить в корневую папку сайта, задать исходную и конечную папку с картинками и запустить, если картинок очень много. Добавить вначале:


ignore_user_abort();
set_time_limit(0);

И запускать из консоли, чтобы видеть этапы работы.


Эксперименты со вставкой логотипа, и подбором степени его прозрачности лучше проводить в конечной папке, для этого в строке $root = $path.”/допишите/свой/путь/до/папки” или положите скрипт в конечную папку и запускайте оттуда.


Мы использовали стандартный логотип компании с прозрачностью 60%.


Заключение


Когда папка с новыми файлами будет готова, Вам остается только переименовать её с img2 в img. В итоге у вас на сайте будет папка с исходными файлами, которые можно за архивировать или удалить и папка с помеченными логотипом фотографиями.


Данный скрипт актуально использовать на проектах, в которых уже загружены изображения товаров или статей, а возможности установить скрипт для добавления водяного знака нет, или вообще не предусмотрено.


Следующей задача, это заменить exif данные на всех картинках этого же сайта. Для чего это и как реализовали, расскажем в следующей статье.

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


  1. Eureka
    28.09.2018 11:36
    +1

    Решал подобную задачу на Apache иначе:
    Все защищенные изображения хранил в недоступной через веб папке. Через простенький .htaccess настроил проверку: если пришел запрос на изображение и его нет в папке кэша (с водяным знаком, правильным размером...) — создаем его на лету и отдаем, иначе апачем же прозрачно редиректим на имеющееся защищенное изображение и отдаем ничего не подозревающему браузеру.


    1. likvp Автор
      28.09.2018 12:17

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


      1. Eureka
        28.09.2018 22:01

        Если защищенное изображение создано — отдаем апачем, если нет, создаём и отдаем через PHP.

        RewriteCond %{REQUEST_URI} ^/images(.*)\.(jpe?g|png)$
        RewriteCond %{DOCUMENT_ROOT}/web%{REQUEST_URI} !-f
        RewriteRule (.*) /web/image.php/$1 [L]
        RewriteCond %{DOCUMENT_ROOT}/web%{REQUEST_URI} -f
        RewriteRule (.*) /web/$1 [L]
        Оригиналы картинок в /images/..., доступные пользователям в /web/images/… Скрипт наложения лого и ресайза /web/image.php. Логика защиты: если файл больше минимального размера, добавляем водяной знак, записываем и отдаем. Логика ресайза: если файла/папки /images/95x30/image.jpg не существует, создаем из /images/image.jpg превьюху размером 95х30, записываем в соответствующую папку и отдаём (можно без папки, но мне больше нравится сохранять неизменным имя файла).


    1. tsukasa_mixer
      28.09.2018 12:25

      аналогично делается на nginx


  1. FanatPHP
    28.09.2018 11:52

    Наши предложения в комментариях:


    $new_dir = null;
    $dir_files = opendir($dir);
    while(false !== ($file = readdir($dir_files)))
    {
    
        if($file != '.' && $file != '..')
            $new_dir[] = $dir."/".$file;
    }

    ->


    $new_dir[] = glob("$dir/*");

    Никогда так не делайте, ни на коленке, ни на станке с программным управлением:


    $new_dir = null;

    Если переменная содержит массив, то и должна быть инициализирована, как массив.


    Заодно пропадет необходимость в лишнем условии if($new_dir)


    Ну и разделить эту кучу-малу не помешало бы.
    одна функция собирает в массив абсолютные пути ко всем файлам в дереве, а вторая накладывает водяной знак. И в аккуратном цикле вызывает вторую для результатов первой.


    1. likvp Автор
      28.09.2018 12:19

      Я обязательно воспользуюсь всеми советами, и усовершенствую данную версию.
      Если есть ещё, прямо кусочки кода которые стоит заменить, сообщите.
      Я не претендую на идеальный продукт, но нужно учиться и стремиться к совершенству.


      1. Afinogen
        28.09.2018 13:07

        Добавлю еще

        $new = str_replace("img","img2",$check); //Заменяем имя папки

        У вас очевидно ни когда не было под папок с именами img* (например img123), так можно изображения потерять, так как они будут по пути img2*


        1. likvp Автор
          28.09.2018 13:13

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


    1. oxidmod
      28.09.2018 14:32

      Еще не плохо бы не в памяти держать все пути, а заюзать генераторы


      1. likvp Автор
        28.09.2018 15:53

        Чуть по подробнее, не понял сразу



  1. NickyX3
    28.09.2018 12:07
    +1

    Жованный крот. Зачем накладывать это в php? Обойти картинки, дернуть размер, потом вызвать
    compose over -quality 100 -geometry +x+y watermark_file src_file dst_file (где x & y позиция куда втыкать ватермарк)
    будет в 100 раз быстрее.

    Да и opendir/readdir тоже сомнительно.
    $scanned_directory = array_diff(scandir($directory), array('..', '.'));
    Решает все чтение папки одной строкой


    1. likvp Автор
      28.09.2018 12:21

      А как вставить в вашу команду, вычисления размера куда вставлять ватермарк и изменении размера ватермарка в зависимости от картинки. В итоге, скорее всего, придется писать скрипт. А тут уж кто на что горазд.
      За $scanned_directory = array_diff(scandir($directory), array('..', '.')); большое спасибо, в этом направлении я не думал…


      1. NickyX3
        28.09.2018 13:04

        Я привел пример команды для утилиты composite из пакета ImageMagick, бинарной консольной утилиты, которая выполнит наложение водяного знака в десять раз быстрее «загрузки» обоих картинок в php с последующим сращиванием.

        Кусок кода для расчета позиции знака ровно посередине

            	// размеры картинки и водяного знака 
            	// ( если размеры знака известны заранее, можно и не получать их вообще)
            	$imagesize    = getimagesize($sourceFile);
            	$wmsize 	    = getimagesize($watermarkfile);
            	// размеры картинки
            	$width 	= $imagesize[0];
            	$height     = $imagesize[1];
            	// размеры знака	
            	$w		= $wmsize[0];
            	$h		= $wmsize[1];
            	//центральная позиция
            	$x 		= $width/2 - $w/2;
        	$y 		= $height/2 - $h/2;
        
        	$command = '/usr/local/bin/composite -compose over -quality 93  -geometry +'.$x.'+'.$y.' '.$watermarkfile.' "'.$sourceFile.'" "'.$destinationFile.'"';
        	exec ($command,$out,$exitcode);
        	// проверка $exitcode уже кому как нравится
        


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


        1. likvp Автор
          28.09.2018 13:47

          А вообще в общем и целом — марки отлично накладываются при желании на лету в том же nginx. И хранить придется только оригиналы картинок

          Вопрос нагрузки, у клиента на странице открывается сразу 40 фотографий. И если все 40 будут на лету обрабатываться и ещё и клиентов будет 100 как бы наш дешевый сервак не упал.
          Интересное решение у Eureka выше, это при запросе создавать файл и уже потом его хранить. Так работает движок OpenCart с кэшированием картинок.
          Кусок кода для расчета позиции знака ровно посередине

          Это просто шикарно, обязательно протестирую!


          1. NickyX3
            28.09.2018 14:26

            Вопрос нагрузки, у клиента на странице открывается сразу 40 фотографий. И если все 40 будут на лету обрабатываться и ещё и клиентов будет 100 как бы наш дешевый сервак не упал.

            В nginx + resize умеет обработанное кешировать. На столько, на сколько скажете. Хотя обычно это применяется только для ресайза, генерации более мелких вариантов оригинальной картинки.


            1. likvp Автор
              28.09.2018 15:54

              Спасибо, для меня это было открытием. Пойду более подробно читать об nginx.


  1. t38c3j
    28.09.2018 13:34

    Почитайте про RecursiveDirectoryIterator и SPL в общем


    1. likvp Автор
      28.09.2018 13:42

      Для обхода директории, есть множество способов.
      В следующем проекте, обязательно воспользуюсь этим.
      А вот про SPL первый раз слышу… Я правильно понял что это: php.net/manual/ru/book.spl.php и как его можно применить?


      1. t38c3j
        28.09.2018 15:12

        Да, правильно поняли. Прочитайте введение.


  1. abadello
    28.09.2018 13:35

    Собственно конечно можно и так, но почему религия мешает сделать копию каталога на сайте, а затем пройти её рекурсивно с обработкой IM?
    cp -R img_cat1 img_cat2
    for i in `ls *.jpg -R`
    do
    echo $i
    composite -resize 800 -watermark 10.0 watermark.png $i $i
    done

    К чему танцы с бубном в гамаке?


    1. likvp Автор
      28.09.2018 13:38

      У вас решение, для которого нужно обладать некоторыми знаниями.
      Мы же старались создать решение для обычного или начинающего пользователя.
      И как уже писали, скорее всего, в этом коде вы не учитываете что картинки будут разного размера, и watermark нужно менять.


      1. abadello
        28.09.2018 13:55

        Ну вопрос конкретного применения ImageMagick я не учитывал, понятно что скрипт может варьироваться, но это не отменяет того, что встроенные инструменты ОС для файловых операций предпочтительней.
        Исходя из php что-то вроде:

        exec('cp -R img_cat1 img_cat2'); 
        

        Будет сильно предпочтительней этого:
        $fileName = basename($check); //Определяем имя файла
        $new = str_replace("img","img2",$check); //Заменяем имя папки
        $put = substr($new,0,-strlen($fileName)); //Определяем путь до папки
        if (!file_exists($put)) {
            mkdir($put, 0777, true); //Если папки нет, то создаем
        }
        


      1. FanatPHP
        28.09.2018 16:19

        Совет: никогда не нужно прикрываться воображаемым "начинащим пользователем", ради которого все ваши старания :)
        Во-первых, это никак не может объяснить плохой код.
        Во-вторых, начинающему тоже надо когда-то начинать расти, и получать эти самые "специальные знания".


  1. boilroom
    28.09.2018 22:24

    Приходилось такое несколько раз решать. Думаю, что лучше было бы реализовать динамическое добавление водяных знаков. Выше это уже предлагалось, но я вставлю свои несколько копеек. Такое решение позволит избежать некоторых проблем:

    1) При добавлении новых изображений не придется прогонять ваш скрипт по новой, заставлять контент-менеджеров лепить на изображения водяные знаки (они сделают это криво) или писать отдельный скрипт, который делает это автоматически при загрузке каждого нового изображения.

    2) Вы предлагаете, если я правильно понимаю, заменить старые изображения без водяных знаков на новые с ними. А старые, «пустые», забэкапить. И после не загружать на сервер изображения без водяного знака (как это у вас будет реализовано — не совсем понятно). А теперь представьте, что сменились требования к размещению или виду водяного знака. В результате вы получите бэкап с некоторой частью изображений без знаков + (возможно) некоторое количество изображений для которых нет копии без знака. И это станет куда большей головной болью, чем ваша изначальная задача.

    И, кстати, бэкапы с «какая-то часть старых данных» теряются обычно в первую очередь.

    Идея хранить исходные изображения без каких-либо водяных знаков и добавлять эти знаки уже только при выводе мне кажется более дальновидной.


    1. likvp Автор
      29.09.2018 11:16

      Оригиналы Изображения хранятся в папке, и работа ведётся с ними, при добавлении новых картинок, стоит прогнать скрипт заново и перегегирировать все картинки.
      При изменении требований к водяному знаку, так же можно внести коррективы в скрипт и прогнать. У нас на проекте в 10к картинок это занимает минуты 2.


      Скрипт, выполняет задачу: добавить ко всем картинкам водяной знак, он ее выполняет. И требует минимальных временных затрат.


      Да почитав комментарии, есть желание его улучшить, ускорить, добавить функций и вариантов.


      Большое спасибо, за развитие идеи.


      1. boilroom
        29.09.2018 16:52

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

        Идея прогонять скрипт каждый раз, да еще и по полной (если я все правильно понял) при появлении каждого нового изображения… Это может быть приемлемым решением, если у вас раз в полгода одно изображение добавляется. Но откуда тогда 10K изображений? Это ведет к:

        1. Появлению на некоторый срок на сайте изображений без водяных знаков: пока скрипт не доработает или если его забыли запустить
        2. Увеличению сложности обслуживания. Контент-менеджеры (мой личный опыт) вечно забывают делать дополнительные действия, если они не совсем очевидны.
        3. Уменьшение надежности всей системы. Ой-ей, храните бэкапы ваших «чистых» картинок. При таких массовых перезаписях однажды может случиться неприятный сюрприз
        4. Создание периодических пиков нагрузки в момент исполнения вашего скрипта
        5. Необходимость хранить два полных комплекта изображений одновременно: комплект с и комплект без водяных знаков.
        6. С ростом количества изображений все неприятные последствия выбранного вами решения будут расти: будет увеличиваться нагрузка на сервер, время обработки и т.д.


        Поэтому я бы рекомендовал применить такой механизм (в общем виде, конкретная реализация зависит от того, какой у вас там стек):

        • Создаем каталог для хранения кэша изображений. Т.е. изображений с водяными знаками
        • При запросе изображения на выдачу выясняем есть ли уже готовое изображение в «кэше»
        • Если есть — отдаем его. Если нет — обрабатываем изображение, сохраняем результат в кэш и отдаем пользователю
        • Периодически чистим кэш. Самый простой вариант — удаляем раз в X минут/часов все файлы старше некоторого N. Но в идеале, наверное, стоит учитывать все-таки не время создания файла, а время последнего его использования. Не запрашивалось ни разу за последние X часов? Удаляем из кэша. Тут надо быть аккуратным, чтобы не сделать какую-нибудь ресурсозатратную жуть в качестве системы учета времени последнего использования.


        Думаю, такой вариант решения задачи будет лучше выбранного вами по практически всем параметрам.


        1. likvp Автор
          29.09.2018 17:00

          Да и это был первый комментарий, и ко второй версии мы это все добавим.


  1. unnforgiven
    01.10.2018 18:47

    Это все делаеться в nginx — зачем городить в скрипте? Лишняя работа по обновлению, как было написано раньше.