Водяные знаки (watermark) для картинок на ASP.NET и GDI+

61

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

Watermark (водяной знак) служит для защиты фотографий от копирования и распространения на других сайтах и/или в других приложениях. Обычно он наноситься в виде полупрозрачной надписи с названием сайта или компании, которая не мешает просмотру фотографии для пользователя, но указывает на ее первоисточник.

Вручную создание watermark делается довольно быстро с использованием таких графических дизайнеров как Photoshop или Gimp. Но такой подход не подходит для сайтов, где зачастую картинки загружаются пользователями и обрабатывать их нужно на сервере. Например, для своего сайта я бы мог использовать следующую подпись, которая находится в правом нижнем углу:

Пример водяного знака

Если вы используете платформу ASP.NET, то для создания водяных знаков можно использовать графическую библиотеку GDI+. Я более подробно рассматривал её использование в статье Графика GDI+. В этой статье я приведу пример создания подписи текстом и логотипом из картинки.

Текстовый водяной знак

Мы продолжим использовать проект UploadFiles из статьи Ajax-загрузка файлов с индикатором. Напомню, в этой статье мы настроили асинхронную AJAX-загрузку картинок на сервер с поддержкой drag–and-drop для файлов и индикатора, отображающего процент загрузки.

Добавьте в проект папку Infrastructure и файл класса Watermark.cs со следующим содержимым:

using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Web;

namespace UploadFiles
{
    public static class Watermark
    {
        /// <summary>
        /// Метод, подготавливающий поверхность для рисования GDI+
        /// </summary>
        /// <returns>Возвращает объект Graphics</returns>
        private static Graphics GdiBase(
            HttpPostedFileBase file, 
            ref Bitmap bitmap,
            out int imageWidth, out int imageHeight)
        {
            // Получить изображение, его ширину и высоту, преобразовать в объект Bitmap
            Image image = Image.FromStream(file.InputStream, true, true);
            imageWidth = image.Width;
            imageHeight = image.Height;

            bitmap = new Bitmap(imageWidth, imageHeight,
                PixelFormat.Format24bppRgb);

            bitmap.SetResolution(image.HorizontalResolution, image.VerticalResolution);

            // Базовый класс GDI+, создающий слой для рисования
            Graphics graphics = Graphics.FromImage(bitmap);

            // Рисуем картинку
            graphics.DrawImage(
                image,
                new Rectangle(0, 0, imageWidth, imageHeight),
                0,
                0,
                imageWidth,
                imageHeight,
                GraphicsUnit.Pixel);

            return graphics;
        }

        // …
    }
}

Мы создали статичный класс Watermark, методы которого будут формировать водяные знаки. В базовом закрытом методе GdiBase() мы загружаем картинку на графический слой GDI+. Класс Graphics, объект которого возвращается этим методом, является ядром GDI+. Обратите внимание, что GdiBase() принимает параметры с модификаторами out и ref, чтобы мы могли получить измененные значения этих параметров в вызывающем методе.

Теперь нам нужно добавить в класс Watermark метод, добавляющий текстовый водяной знак:

/// <summary>
/// Метод для добавления текстовой подписи
/// </summary>
/// <param name="file">Файл загруженной картинки</param>
/// <param name="text">Текст подписи</param>
/// <param name="pathSave">Путь для сохранения картинки</param>
/// <param name="fontName">Шрифт текста подписи (по умолчанию Arial)</param>
public static void SetText(HttpPostedFileBase file, string text,
    string pathSave, string fontName = "arial")
{
    // Поолучаем объект Graphics
    Bitmap bitmap = null;
    int imageWidth = 0, imageHeight = 0;

    using (Graphics graphics = GdiBase(file, ref bitmap, out imageWidth, out imageHeight))
    {
        // Задаем качество рендеринга для картинки
        graphics.SmoothingMode = SmoothingMode.AntiAlias;

        // Подбираем размер шрифта, чтобы подпись полность помещалась на картинке
        int[] fontSizes = new int[] { 16, 14, 12, 10, 8, 6, 4 };
        Font font = null;
        SizeF size = new SizeF();
        for (int i = 0; i < 7; i++)
        {
            font = new Font(fontName, fontSizes[i], FontStyle.Bold);
            size = graphics.MeasureString(text, font);

            if ((ushort)size.Width < (ushort)imageWidth)
                break;
        }

        // Добавляем смещение 5% относительно низа экрана и выравниваем по центру
        int yPixelsFromBottom = (int)(imageHeight * 0.05);
        float positionY = ((imageHeight -
                    yPixelsFromBottom) - (size.Height / 2));
        float centerX = (imageWidth / 2);

        StringFormat stringFormat = new StringFormat();
        stringFormat.Alignment = StringAlignment.Center;

        // Полупрозрачная кисть черного цвета для обводки текста
        SolidBrush brush2 = new SolidBrush(Color.FromArgb(152, 0, 0, 0));

        graphics.DrawString(text,
            font,
            brush2,
            new PointF(centerX + 1, positionY + 1),
            stringFormat);

        // Полупрозрачная кисть белого цвета для заливки текста
        SolidBrush brush = new SolidBrush(
                        Color.FromArgb(153, 255, 255, 255));

        graphics.DrawString(text,
            font,
            brush,
            new PointF(centerX, positionY),
            stringFormat);

        // Сохранить картинку
        bitmap.Save(pathSave,
            // Выбор формата для сохранения на основе MIME
            file.ContentType.Contains("png") ? ImageFormat.Png : ImageFormat.Jpeg);
    }
}

Давайте разберем этот код более подробно. Чтобы увеличить размер текстового сообщения, мы проверяем 7 разных размеров шрифта (от 16 до 4 pt), чтобы определить максимально возможный размер, который мы можем использовать для ширины нашей фотографии. Мы определили массив целых чисел, а затем измерили в цикле длину строки с каждым размером шрифта. Как только длина строки становится меньше длины картинки, мы прерываем цикл оператором break и получаем оптимальный размер шрифта.

Поскольку все фотографии будут иметь разную высоту, мы расположили текст снизу и задали отступ для текста в 5% от высоты картинки. Используя значение высоты строки (size.Height) мы получили координату positionY для расположения водяного знака по оси Y. По оси X надпись будет располагаться по центру, поэтому мы просто разделили ширину картинки пополам.

После того, как получены все необходимые координаты позиционирования, создается кисть brush2 и отрисовывается текст со смещением в 1 пиксель вправо и вниз. Это сделано для того, чтобы создать контур текста. Текст, имеющий темный контур и светлую заливку, будет виден на любом изображении. В конце мы сохраняем картинку, определяя формат изображения на основе полученного MIME-типа.

Давайте теперь протестируем работу программы. Напомню, что процесс сохранения картинки у нас происходит в методе Upload() контроллера HomeController. Нам необходимо изменить небольшой кусок кода, где мы сохраняли картинку:

// …

namespace UploadFiles.Controllers
{
    public class HomeController : Controller
    {
        // …

        [HttpPost]
        public JsonResult Upload()
        {
            // …

            // Сохранить файл и вернуть URL
            if (Directory.Exists(__filepath))
            {
                 Guid guid = Guid.NewGuid();
                 string path = $@"{__filepath}\{guid}.{file.FileName}";

                 // Добавить подпись для картинки и сохранить
                 Watermark.SetText(file, "professorweb.ru", path);

                 result.Files.Add($"/uploads/{guid}.{file.FileName}");
             }

            // …
        }
    }

    // …
}

Запустите приложение и попробуйте сохранить несколько изображений:

Пример сохранения картинок с текстовым водяным знаком

На рисунке ниже я открыл каждую сохраненную картинку в новом окне, для наглядности:

Сгенерированный текстовый водяной знак на ASP.NET

Как видно, на каждое загруженное изображение были наложены водяные знаки.

Водяной знак из картинки

Многие разработчики вместо статичного текста добавляют логотип компании в качестве подписи. Давайте расширим наш класс Watermark и добавим метод SetImage():

/// <summary>
/// Метод для добавления подписи-картинки
/// </summary>
/// <param name="file">Файл загруженной картинки</param>
/// <param name="pathWatermark">Путь до картинки подписи</param>
/// <param name="pathSave">Путь для сохранения картинки</param>
public static void SetImage(HttpPostedFileBase file,
    string pathWatermark, string pathSave)
{
    // Настраиваем рисование для базовой картинки
    Bitmap bitmap = null;
    int imageWidth = 0, imageHeight = 0;

    using (Graphics graphics = GdiBase(file, ref bitmap, out imageWidth, out imageHeight))
    {
        // Получение изображения watermark
        Image imageWatermark = new Bitmap(pathWatermark);
        int watermarkWidth = imageWatermark.Width,
            watermarkHeight = imageWatermark.Height;

        // Добавить полупрозрачность для водяного знака
        // 1. Задаем прозрачный фон
        ImageAttributes attrs = new ImageAttributes();
        ColorMap colorMap = new ColorMap();

        colorMap.NewColor = Color.FromArgb(0, 0, 0, 0);
        colorMap.OldColor = Color.FromArgb(255, 0, 255, 0);
        ColorMap[] remapTable = { colorMap };

        attrs.SetRemapTable(remapTable, ColorAdjustType.Bitmap);

        // 2. Изменяем прозрачность картинки, используя матрицу
        float[][] colorMatrixArr = {
            new float[] {1.0f,  0.0f,  0.0f,  0.0f, 0.0f},
            new float[] {0.0f,  1.0f,  0.0f,  0.0f, 0.0f},
            new float[] {0.0f,  0.0f,  1.0f,  0.0f, 0.0f},
            new float[] {0.0f,  0.0f,  0.0f,  0.3f, 0.0f},
            new float[] {0.0f,  0.0f,  0.0f,  0.0f, 1.0f}
        };

        ColorMatrix colorMatrix = new ColorMatrix(colorMatrixArr);

        attrs.SetColorMatrix(colorMatrix,
            ColorMatrixFlag.Default,
            ColorAdjustType.Bitmap);

        // Позиционируем водяной знак
        int positionX = ((imageWidth - watermarkWidth) - 10);
        int positionY = 10;

        // Рисуем водяной знак на картинке
        graphics.DrawImage(imageWatermark,
            new Rectangle(positionX, positionY,
                watermarkWidth, watermarkHeight),
            0,
            0,
            watermarkWidth,
            watermarkHeight,
            GraphicsUnit.Pixel,
            attrs);

        // Сохраняем картинку
        bitmap.Save(pathSave,
            // Выбор формата для сохранения на основе MIME
            file.ContentType.Contains("png") ? ImageFormat.Png : ImageFormat.Jpeg);
    }
}

Чтобы достичь полупрозрачного водяного знака, мы применим две цветовые манипуляции, определив объект ImageAttributes и установив два его свойства. Первым шагом является замена цвета фона на прозрачный (Alpha = 0, R = 0, G = 0, B = 0). Для этого мы использовали массив объектов ColorMap. Например, если водяной знак будет находиться на зеленом фоне, этот код сделает его полностью прозрачным.

Вторая цветовая манипуляция используется для изменения прозрачности водяного знака. Это делается путем применения матрицы 5x5, содержащей координаты для пространства RGBA. Установив значение в позиции [3,3] на 0,3f, мы достигнем сильного уровня прозрачности. Результатом является водяной знак, который слегка показывает основное изображение.

В отличие от примера с текстом, здесь мы разместили водяной знак в правом верхнем углу с отступом в 10 пикселей.

Теперь закомментируйте вызов метода SetText() в методе Upload контроллера Home и замените его на SetImage():

namespace UploadFiles.Controllers
{
    public class HomeController : Controller
    {
        // …

        [HttpPost]
        public JsonResult Upload()
        {
            // …

            // Сохранить файл и вернуть URL
            if (Directory.Exists(__filepath))
            {
                 Guid guid = Guid.NewGuid();
                 string path = $@"{__filepath}\{guid}.{file.FileName}";

                 // Добавить подпись для картинки и сохранить
                 // Watermark.SetText(file, "professorweb.ru", path);
                 Watermark.SetImage(file, Server.MapPath("~/Content/watermark.png"), path);

                 result.Files.Add($"/uploads/{guid}.{file.FileName}");
             }

            // …
        }
    }

    // …
}

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

Пример сохранения картинок с водяным знаком из картинки
Пройди тесты
Лучший чат для C# программистов