给图片添加文字水印

功能需求

  1. 在图片的给定位置上添加文字水印
  2. 水印可以旋转和设置透明度

先说说自己的实现思路:

  1. 先创建具有透明背景色的文字水印图像
  2. 将水印图像添加到原图像中

实现

首先创建一个接口,用于约束水印的创建方式:

public interface IWatermark
{
    Bitmap CreateWatermark(string markText, Font font, Brush brush, Rectangle rectangle);
}

具体实现:

public class Watermark : IWatermark
{
    //水印画布
    protected virtual Rectangle WatermarkCanvas { set; get; }

    protected Watermark(){}

    public Watermark(string markText, Font font)
    {
        int width = (int)((markText.Length + 1) * font.Size);
        int height = font.Height;
        WatermarkCanvas = new Rectangle(0, 0, width, height);
    }

    /// <summary>
    /// 给图片添加水印,文字大小以像素(Pixel)为计量单位
    /// </summary>
    /// <param name="filename">图片文件全名</param>
    public Bitmap Mark(string filename, string markText, Font font, Brush brush, float positionX, float positionY, int angle, int transparency)
    {
        return CreateMarkCore(filename, markText, font, brush, positionX, positionY, angle, transparency);
    }

    /// <summary>
    /// 绘制文字水印,文字大小以像素(Pixel)为计量单位
    /// </summary>
    public virtual Bitmap CreateWatermark(string markText, Font font, Brush brush, Rectangle rectangle)
    {
        Bitmap watermark = new Bitmap(rectangle.Width, rectangle.Height);
        Graphics graphics = Graphics.FromImage(watermark);
        //消除锯齿
        graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
        graphics.DrawString(markText, font, brush, rectangle);
        graphics.Dispose();
        return watermark;
    }

    /// <summary>
    /// 给图片添加水印,文字大小以像素(Pixel)为计量单位
    /// </summary>
    /// <param name="filename">图片文件全名</param>
    protected virtual Bitmap CreateMarkCore(string filename, string markText, Font font, Brush brush, float positionX, float positionY, int angle, int transparency)
    {
        if (!File.Exists(filename))
        {
            throw new FileNotFoundException("文件不存在!");
        }
        Bitmap resultImg;
        using (Bitmap rawImg = new Bitmap(filename))
        {
            using (Bitmap watermarkImg = CreateWatermark(markText, font, brush, WatermarkCanvas))
            using (Bitmap rotateImg = Rotate(watermarkImg, angle))
            {
                using (Bitmap temp = SetAlpha(rotateImg, transparency))
                {
                    resultImg = new Bitmap(rawImg.Width, rawImg.Height);
                    using (Graphics newGraphics = Graphics.FromImage(resultImg))
                    {
                        newGraphics.DrawImage(rawImg, 0, 0);
                        newGraphics.DrawImage(temp, positionX, positionY);
                    }
                }
            }
        }
        return resultImg;
    }
}

水印图片透明度设置和旋转(下面这段代码和上面一段代码都位于Watermark类中,因为代码量较大,所以分开来展示):

public class Watermark : IWatermark
{
        protected Bitmap Rotate(Bitmap rawImg, int angle)
        {
            angle = angle % 360;
            //弧度转换
            double radian = TranslateAngleToRadian(angle);
            //原图的宽和高
            int width = rawImg.Width;
            int height = rawImg.Height;
            //旋转之后图像的宽和高
            Rectangle rotateRec = RecalculateRectangleSize(width, height, angle);
            int rotateWidth = rotateRec.Width;
            int rotateHeight = rotateRec.Height;
            //目标位图
            Bitmap targetImg = new Bitmap(rotateWidth, rotateHeight);
            Graphics targetGraphics = Graphics.FromImage(targetImg);
            //计算偏
            Point Offset = new Point((rotateWidth - width) / 2, (rotateHeight - height) / 2);
            //构造图像显示区域:让图像的中心与窗口的中心点一致
            Rectangle rect = new Rectangle(Offset.X, Offset.Y, width, height);
            Point centerPoint = new Point(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
            targetGraphics.TranslateTransform(centerPoint.X, centerPoint.Y);
            targetGraphics.RotateTransform(angle);
            //恢复图像在水平和垂直方向的平移
            targetGraphics.TranslateTransform(-centerPoint.X, -centerPoint.Y);
            targetGraphics.DrawImage(rawImg, rect);
            //重至绘图的所有变换
            targetGraphics.ResetTransform();
            targetGraphics.Save();
            targetGraphics.Dispose();
            return targetImg;
        }

        /// <summary>
        /// 设置图像透明度,0:全透明,255:不透明
        /// </summary>
        protected Bitmap SetAlpha(Bitmap rawImg, int alpha)
        {
            if (!(0 <= alpha) && alpha <= 255)
            {
                throw new ArgumentOutOfRangeException("alpha ranges from 0 to 255.");
            }
            //颜色矩阵
            float[][] matrixItems =
            {
                new float[]{1,0,0,0,0},
                new float[]{0,1,0,0,0},
                new float[]{0,0,1,0,0},
                new float[]{0,0,0,alpha/255f,0},
                new float[]{0,0,0,0,1}
            };
            ColorMatrix colorMatrix = new ColorMatrix(matrixItems);
            ImageAttributes imageAtt = new ImageAttributes();
            imageAtt.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
            Bitmap resultImg = new Bitmap(rawImg.Width, rawImg.Height);
            Graphics g = Graphics.FromImage(resultImg);
            g.DrawImage(rawImg, new Rectangle(0, 0, rawImg.Width, rawImg.Height),
                    0, 0, rawImg.Width, rawImg.Height, GraphicsUnit.Pixel, imageAtt);
            g.Dispose();

            return resultImg;
        }

        protected double TranslateAngleToRadian(float angle)
        {
            return angle * Math.PI / 180;
        }

        protected Rectangle RecalculateRectangleSize(int width, int height, float angle)
        {
            double radian = TranslateAngleToRadian(angle);
            double cos = Math.Cos(radian);
            double sin = Math.Sin(radian);
            double newWidth = (int)(Math.Max(Math.Abs(width * cos - height * sin), Math.Abs(width * cos + height * sin)));
            double newHeight = (int)(Math.Max(Math.Abs(width * sin - height * cos), Math.Abs(width * sin + height * cos)));
            return new Rectangle(0, 0, (int)newWidth, (int)newHeight);
        }

    }

Watermark类对外暴露了API:Bitmap Mark(string filename, string markText, Font font, Brush brush, float positionX, float positionY, int angle, int transparency),向图片中添加水印只需创建Watermark实例,然后调用该方法即可。具体实现代码如下:

//.NET中,Font尺寸的默认单位是Point,这里统一使用Pixel作为计量单位
string path = @"C:\Users\chiwenjun\Desktop\1.PNG";
string markText = "字体:微软雅黑";
Font font = new Font("微软雅黑", 40, FontStyle.Bold, GraphicsUnit.Pixel);
Watermark watermark = new Watermark(markText, font);
Bitmap img = watermark.Mark(path, markText, font, new SolidBrush(Color.FromArgb(0, 0, 0)), 160, 535, 0, 180);
img.Save(path, ImageFormat.Png);
原图
添加水印效果图
水印顺时针旋转55<sup>0</sup>效果

旋转前后,水印图像的宽和高会发生变化,如下图所示:

水印图片旋转前后宽高变化

扩展

上面的代码很好的实现了在图片上添加单行水印的效果,若要实现多行水印可以通过对Watermark类的扩展来实现。
创建类MultiLineWatermark继承自Watermark,然后覆写属性WatermarkCanvas来指定水印画布的大小;覆写方法CreateWatermark来实现多行水印效果。


    public class MultiLineWatermark : Watermark
    {
        protected int _canvasWidth = 0;
        protected int _canvasHeight = 0;
        //每行水印所允许的最大字数
        protected int _lineMaxLength = 0;
        //水印所允许的最大字数
        protected int _wordMaxLength = 0;

        protected override Rectangle WatermarkCanvas
        {
            get
            {
                return new Rectangle(0, 0, this._canvasWidth, this._canvasHeight);
            }
        }

        public MultiLineWatermark(int canvasWidth, int canvasHeight, int lineMaxLength, int wordMaxLength)
        {
            this._canvasWidth = canvasWidth;
            this._canvasHeight = canvasHeight;
            this._lineMaxLength = lineMaxLength;
            this._wordMaxLength = wordMaxLength;
        }

        public override Bitmap CreateWatermark(string markText, Font font, Brush brush, Rectangle rectangle)
        {
            Bitmap watermark = new Bitmap(rectangle.Width, rectangle.Height);
            Graphics graphics = Graphics.FromImage(watermark);
            //消除锯齿
            graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
            int lineHeight = _canvasHeight / (_wordMaxLength / _lineMaxLength);
            if (markText.Contains('#'))
            {
                string[] textList = markText.Split('#');
                int count = (int)Math.Min(textList.Length, Math.Ceiling(_wordMaxLength * 1.0 / _lineMaxLength));
                for (int i = 0; i < count; i++)
                {
                    if (textList[i].Length > _lineMaxLength)
                    {
                        textList[i] = textList[i].Substring(0, _lineMaxLength);
                    }
                    //文字居中
                    graphics.DrawString(textList[i], font, brush, (rectangle.Width - textList[i].Length * font.Size) / 2, i * lineHeight);
                }
            }
            else
            {
                //文字居中
                if (markText.Length <= _lineMaxLength)
                {
                    graphics.DrawString(markText, font, brush, (rectangle.Width - markText.Length * font.Size) / 2, 0);
                }
                else
                {
                    int count = (int)Math.Min(Math.Ceiling(_wordMaxLength * 1.0 / _lineMaxLength), Math.Ceiling(markText.Length * 1.0 / _lineMaxLength));
                    string[] temp = new string[count];
                    for (int i = 0; i < count; i++)
                    {
                        if (i * _lineMaxLength + _lineMaxLength <= markText.Length)
                        {
                            temp[i] = markText.Substring(i * _lineMaxLength, _lineMaxLength);
                        }
                        else
                        {
                            temp[i] = markText.Substring(i * _lineMaxLength, markText.Length - i * _lineMaxLength);
                        }
                        graphics.DrawString(temp[i], font, brush, (rectangle.Width - temp[i].Length * font.Size) / 2, i * lineHeight);
                    }
                }
            }
            graphics.Dispose();
            return watermark;
        }
    }

具体的使用方式和调用Watermark类似,代码如下:

string path = @"C:\Users\chiwenjun\Desktop\1.PNG";
//以#作为换行标记
string markText = "字体:#微软雅黑雅黑雅黑";
Font font = new Font("微软雅黑", 40, FontStyle.Bold, GraphicsUnit.Pixel);
//若字数超过每行所允许的最大值,超出部分被忽略
int lineMaxLength = 7;
//超出的字数会被忽略
int wordMaxLength = 14;
//行高,用于计算水印图像的高
int lineHeight = 55;
int width = (int)((lineMaxLength + 1) * font.Size);
int height = (int)(Math.Ceiling(wordMaxLength * 1.0 / lineMaxLength) * lineHeight);
Watermark watermark = new MultiLineWatermark(width, height, lineMaxLength, wordMaxLength);
Bitmap img = watermark.Mark(path, markText, font, new SolidBrush(Color.FromArgb(0, 0, 0)), 150, 535, 0, 180);
img.Save(path, ImageFormat.Png);

多行水印的文字是居中显示的:

多行水印效果图

若没有使用#标记换行,当一行字数超过指定的最大字数时,会自动换行。

自动换行效果

这篇文章是对自己项目中添加水印功能的记录,通篇以代码为主,看起来可能会感觉比较枯燥。
功能的实现没有太多难点,唯有一点感受较深,就是水印图像宽和高的计算。.NET(.NET Framework 4.5)中字体大小的度量单位默认是Point,而图像的度量单位是Pixel,单位的不同导致水印图像尺寸的计算出现偏差,这一点折磨我很久。
图像旋转和透明度设置的两个方法RotateSetAlpha是在网友代码基础上修改得到,非本人原创,代码原文已在参考文章中列出,在此对两位网友表示感谢。

参考文章:

C#图像旋转
设置图片透明度的四种方法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容