Как автоматически поставить водяной знак на изображения на сервере

1054
Как автоматически поставить водяной знак на изображения на сервере

Проще всего добавлять водяной знак в момент загрузки изображения. Сервер принимает исходный файл, проверяет формат и размер, сохраняет оригинал в закрытом каталоге, а затем создает публичную копию с логотипом или текстом. Для Linux-сервера я обычно беру ImageMagick. Утилита работает из командной строки, поддерживает JPEG, PNG, WebP и легко встраивается в PHP, Python, Node.js или фоновый обработчик.

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

Устанавливаем ImageMagick и проверяем одну картинку

На Ubuntu или Debian я устанавливаю пакет следующей командой.

sudo apt update
 sudo apt install imagemagick
 magick -version

На AlmaLinux, Rocky Linux или Fedora команда выглядит так.

sudo dnf install ImageMagick
 magick -version

В старых сборках ImageMagick 6 вместо команды magick могут использоваться отдельные команды convert, composite и identify. Ниже я использую синтаксис ImageMagick 7. Операции наложения и параметры прозрачности описаны в документации ImageMagick.

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

Для быстрой проверки можно выполнить такую команду.

magick input.jpg 
   ( watermark.png -resize 320x 
      -channel A -evaluate multiply 0.35 +channel ) 
   -gravity southeast 
   -geometry +24+24 
   -composite 
   -quality 88 
   output.jpg

Команда уменьшает знак до ширины 320 пикселей, снижает непрозрачность до 35 процентов, ставит логотип в правый нижний угол и оставляет отступ 24 пикселя. Исходный файл input.jpg не меняется. Результат записывается в output.jpg.

Положение задает параметр -gravity. Часто нужны следующие значения.

Значение Положение
northwest Левый верхний угол
northeast Правый верхний угол
center Центр изображения
southwest Левый нижний угол
southeast Правый нижний угол

Фиксированная ширина подходит только для картинок примерно одного размера. Логотип шириной 320 пикселей будет огромным на фотографии 640 на 480 и почти незаметным на изображении 6000 на 4000. Поэтому на рабочем сервере я рассчитываю ширину знака относительно исходного файла.

width=$(magick identify -format '%w' input.jpg)
 watermark_width=$((width / 5))
 
 magick input.jpg 
   ( watermark.png -resize "${watermark_width}x" 
      -channel A -evaluate multiply 0.35 +channel ) 
   -gravity southeast 
   -geometry +24+24 
   -composite 
   -quality 88 
   output.jpg

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

Текстовый знак можно создать без отдельного PNG.

magick input.jpg 
   -gravity southeast 
   -fill 'rgba(255,255,255,0.60)' 
   -stroke 'rgba(0,0,0,0.35)' 
   -strokewidth 2 
   -pointsize 36 
   -annotate +24+24 'example.ru' 
   -quality 88 
   output.jpg

Темная обводка сохраняет читаемость белой надписи на светлом фоне. На сервере должен быть установлен выбранный шрифт. Список доступных шрифтов показывает команда magick -list font.

Обрабатываем каталог и сохраняем структуру файлов

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

sudo mkdir -p /var/www/watermarked
 sudo chown www-data:www-data /var/www/watermarked

Следующий Bash-скрипт находит JPEG, PNG и WebP, повторяет исходную структуру каталогов и создает копии с водяным знаком.

#!/usr/bin/env bash
 
 set -u
 
 SOURCE_DIR="/var/www/uploads"
 OUTPUT_DIR="/var/www/watermarked"
 WATERMARK="/opt/watermark/logo.png"
 
 find "$SOURCE_DIR" -type f 
   ( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' ) 
   -print0 |
 while IFS= read -r -d '' file
 do
     relative="${file#"$SOURCE_DIR"/}"
     output="$OUTPUT_DIR/$relative"
 
     mkdir -p "$(dirname "$output")"
 
     width=$(magick identify -format '%w' "$file" 2>/dev/null) || {
         echo "Не удалось прочитать $file" >&2
         continue
     }
 
     watermark_width=$((width / 5))
 
     magick "$file" 
       -auto-orient 
       ( "$WATERMARK" -resize "${watermark_width}x" 
          -channel A -evaluate multiply 0.35 +channel ) 
       -gravity southeast 
       -geometry +24+24 
       -composite 
       -strip 
       -quality 88 
       "$output" || {
         echo "Ошибка обработки $file" >&2
         continue
     }
 
     echo "Создан $output"
 done

Сохраняю скрипт как /usr/local/bin/add-watermarks.sh, выдаю право на запуск и проверяю вручную.

sudo chmod 750 /usr/local/bin/add-watermarks.sh
 sudo /usr/local/bin/add-watermarks.sh

Параметр -auto-orient применяет ориентацию из EXIF до наложения знака. Без такой операции фотография со смартфона иногда поворачивается уже после обработки, поэтому логотип оказывается не в том углу. Параметр -strip удаляет EXIF, GPS и другую метаинформацию. Если сайту нужны дата съемки, авторские поля или цветовой профиль, не удаляйте метаданные без разбора.

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

if [[ -f "$output" && "$output" -nt "$file" && "$output" -nt "$WATERMARK" ]]
 then
     continue
 fi

Проверку нужно поставить после вычисления переменной output. Скрипт пропустит готовую копию, когда исходник и логотип не менялись.

Добавляем водяной знак при загрузке через PHP

Для сайта на PHP я предпочитаю расширение Imagick. Обработчик получает уже проверенный локальный файл, вычисляет размер логотипа, накладывает PNG и записывает результат в публичный каталог.

<?php
 
 declare(strict_types=1);
 
 function addWatermark(
     string $source,
     string $watermarkFile,
     string $destination
 ): void {
     if (!is_file($source) || !is_readable($source)) {
         throw new RuntimeException('Исходное изображение недоступно');
     }
 
     if (!is_file($watermarkFile) || !is_readable($watermarkFile)) {
         throw new RuntimeException('Файл водяного знака недоступен');
     }
 
     $image = new Imagick($source);
     $watermark = new Imagick($watermarkFile);
 
     $image->autoOrient();
 
     $imageWidth = $image->getImageWidth();
     $imageHeight = $image->getImageHeight();
 
     if ($imageWidth > 10000 || $imageHeight > 10000) {
         throw new RuntimeException('Слишком большие размеры изображения');
     }
 
     $targetWidth = max(120, (int) round($imageWidth * 0.20));
 
     $watermark->thumbnailImage($targetWidth, 0, true, true);
     $watermark->evaluateImage(
         Imagick::EVALUATE_MULTIPLY,
         0.35,
         Imagick::CHANNEL_ALPHA
     );
 
     $margin = max(16, (int) round($imageWidth * 0.015));
 
     $x = $imageWidth - $watermark->getImageWidth() - $margin;
     $y = $imageHeight - $watermark->getImageHeight() - $margin;
 
     if ($x < 0 || $y < 0) {
         throw new RuntimeException('Водяной знак больше изображения');
     }
 
     $image->compositeImage(
         $watermark,
         Imagick::COMPOSITE_OVER,
         $x,
         $y
     );
 
     $image->stripImage();
 
     if (strtoupper($image->getImageFormat()) === 'JPEG') {
         $image->setImageCompression(Imagick::COMPRESSION_JPEG);
         $image->setImageCompressionQuality(88);
     }
 
     $directory = dirname($destination);
 
     if (!is_dir($directory) && !mkdir($directory, 0750, true)) {
         throw new RuntimeException('Не удалось создать каталог');
     }
 
     if (!$image->writeImage($destination)) {
         throw new RuntimeException('Не удалось сохранить изображение');
     }
 
     $watermark->clear();
     $image->clear();
 }
 
 addWatermark(
     '/srv/private-originals/photo.jpg',
     '/opt/watermark/logo.png',
     '/var/www/site/public/media/photo.jpg'
 );

Расширение можно установить через пакетный менеджер. Названия пакетов зависят от версии PHP и дистрибутива.

sudo apt install php-imagick
 sudo systemctl restart php8.3-fpm

Номер службы нужно заменить на установленную версию PHP. Проверить расширение можно командой php -m | grep imagick.

Обработчик загрузки не должен доверять расширению файла. Файл с именем photo.jpg может оказаться архивом, SVG-документом или другим содержимым. Я проверяю MIME через finfo, ограничиваю размер запроса, разрешаю только нужные растровые форматы и генерирую собственное имя файла.

$allowedTypes = [
     'image/jpeg' => 'jpg',
     'image/png'  => 'png',
     'image/webp' => 'webp',
 ];
 
 $finfo = new finfo(FILEINFO_MIME_TYPE);
 $mime = $finfo->file($uploadedTmpFile);
 
 if (!isset($allowedTypes[$mime])) {
     throw new RuntimeException('Недопустимый формат');
 }
 
 if (filesize($uploadedTmpFile) > 15 * 1024 * 1024) {
     throw new RuntimeException('Файл слишком большой');
 }
 
 $safeName = bin2hex(random_bytes(16)) . '.' . $allowedTypes[$mime];

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

Как построить рабочую схему и не положить сервер

Для небольшого сайта PHP может обработать изображение прямо во время загрузки. При высоком трафике такой подход увеличивает время ответа и расходует память процесса. Фотография размером 20 мегапикселей занимает в памяти намного больше, чем сжатый JPEG на диске. Несколько параллельных загрузок способны занять сотни мегабайт.

На нагруженном проекте я делю процесс на этапы.

  1. Приложение проверяет загрузку и сохраняет оригинал в закрытом хранилище.
  2. Приложение создает задачу в очереди.
  3. Фоновый обработчик создает версии нужного размера и накладывает водяной знак.
  4. Готовые файлы попадают в публичный каталог или объектное хранилище.
  5. CDN раздает уже обработанные изображения.

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

Стандартный модуль Nginx image_filter умеет менять размер, обрезать и поворачивать изображения, но не предназначен для наложения произвольного PNG-логотипа. Для водяного знака нужен отдельный обработчик, ImageMagick, библиотека приложения или специализированный сервер изображений.

С ImageMagick я также ограничиваю память, время работы и доступные форматы. Настройки хранятся в policy.xml. Расположение файла показывает команда magick -list policy. Конкретные лимиты зависят от объема памяти и числа параллельных процессов, но отправная конфигурация может выглядеть так.

<policymap>
   <policy domain="resource" name="memory" value="256MiB"/>
   <policy domain="resource" name="map" value="512MiB"/>
   <policy domain="resource" name="disk" value="1GiB"/>
   <policy domain="resource" name="width" value="12KP"/>
   <policy domain="resource" name="height" value="12KP"/>
   <policy domain="resource" name="time" value="30"/>
 
   <policy domain="coder" rights="none" pattern="PDF"/>
   <policy domain="coder" rights="none" pattern="PS"/>
   <policy domain="coder" rights="none" pattern="EPS"/>
   <policy domain="coder" rights="none" pattern="XPS"/>
   <policy domain="coder" rights="none" pattern="SVG"/>
   <policy domain="coder" rights="read|write" pattern="{JPEG,PNG,WEBP}"/>
 </policymap>

Не копируйте конфигурацию поверх системного файла без резервной копии. Сначала проверьте путь, текущие правила и форматы, которые действительно использует проект. Запрет SVG или PDF сломает обработку таких документов, если приложение принимает их намеренно.

Чаще всего водяной знак работает плохо по следующим причинам.

  • Сайт изменяет оригинал и теряет возможность пересоздать изображения.
  • Логотип имеет фиксированный размер и выглядит по-разному на маленьких и больших фотографиях.
  • Прозрачность уже записана в PNG, а скрипт повторно уменьшает альфа-канал.
  • Каждая операция заново пережимает JPEG и постепенно ухудшает качество.
  • Картинки обрабатываются при каждом запросе без дискового или CDN-кэша.
  • Сервер принимает любой формат и не ограничивает размеры изображения.
  • После замены логотипа CDN продолжает отдавать старую копию.
  • CSS-слой принимают за защиту файла, хотя исходник остается доступным.

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

Для большинства сайтов оптимальна простая схема. Оригинал хранится отдельно, обработчик один раз создает публичную копию, логотип занимает около 15-20 процентов ширины, а CDN раздает готовый файл. Водяной знак не предотвращает кражу полностью, поскольку изображение можно обрезать или отредактировать. Знак скорее показывает источник, затрудняет автоматическое копирование и сохраняет авторство при обычных перепубликациях. Для серьезной защиты нужны учет происхождения файлов, метаданные, мониторинг копий и готовность подтверждать права на оригинал.

водяной знак изображение сервер ImageMagick PHP Imagick логотип автоматизация
Alt text
Обращаем внимание, что все материалы в этом блоге представляют личное мнение их авторов. Редакция SecurityLab.ru не несет ответственности за точность, полноту и достоверность опубликованных данных. Вся информация предоставлена «как есть» и может не соответствовать официальной позиции компании.
Security Vision
Вебинар · 30.07.2026 · 11:00
Приоритеты по критичности обман?
Десятки тысяч уязвимостей, но опасна лишь одна. Узнайте на бесплатном вебинаре Security Vision 30 июля, как видеть всю инфраструктуру, верно расставлять приоритеты и не срывать SLA.
Участие бесплатное
18+. Реклама. Рекламодатель ООО «Интеллектуальная безопасность», ИНН 7719435412