Click here to Skip to main content
Click here to Skip to main content

Creating Semi-Transparent Watermarks for Images in VB.NET

, 9 Feb 2005
Rate this:
Please Sign up or sign in to vote.
Apply a translucent watermark to a JPEG image using VB.NET.

Introduction

A frequent need in graphics processing is that of adding a "watermark" of some sort to an image. I'm not talking about digital signatures or machine-readable hidden text here, though some image processing applications use the term "watermark" to refer to those. Here, I'm just referring to some sort of visible copyright notice, logo, bug, or text -- basically, any sort of visual device that says "hey, this image belongs to so and so," as a small measure of copyright protection that can be applied to the image itself.

In the days of VB 4, 5 and 6, accomplishing this would have been a fairly involved undertaking. Today, however, the GDI+ components in the .NET framework eliminate majority of the difficulty in a task like this. In fact, the code module for my own watermark helper class contains a whopping 122 lines of code, including white space, unused functions and some leftover test code. That shouldn't be too daunting, now should it? There are already several articles available on popular developer sites describing how to add a watermark to an image, so instead of rehashing what's already out there, I want to describe a specific scenario I needed to deal with, deal with some issues that needed addressing in other approaches I encountered, then describe the approach that worked best for addressing my scenario.

Background and History

Here are two scenarios that might call for adding a watermark to an image. Suppose someone wants to post their digital pictures to a photo hosting site, but they don't want their images being swiped by others and used without their knowledge, and they also don't want to have to keep reminding Aunt Edna where their family photo albums can be found on the internet. Or suppose your client is a professional photographer who wants a way to offer proofs to clients that show off the full glory of his ultra-high-resolution equipment, yet he doesn't want to risk having those proofs used by the customer without paying for the images. In either scenario, a watermark would solve the problem nicely. Our need is to add some simple identifying information to the image - perhaps a business name, a logo, or the URL of the photo album - which would be difficult to remove or obscure. Most folks don't work with PhotoShop, Paint Shop Pro or the like on a regular basis, and similarly most folks don't want to take the time or effort to acquire and learn these apps. In this case, I thought the scenario justified creating a simple application that would de-skill the process of adding watermarks to images. Let's have a look at the requirements:

  1. The image watermark has to be prominent and difficult to remove by e.g. simply cropping it out of the photo. Having the watermark overlay the image translucently (like a network bug in the bottom-right corner of your TV screen) seems to be a fairly straightforward way of achieving this.
  2. The watermark needs to be generated from user-entered text.
  3. Since most casual users don't know how to use Char map or [Alt+0169] to generate a proper © copyright symbol, we could append this automatically for the user.
  4. This is a quickie project, so the UI should be very simple, with most functions handled "automagically" for the user, like:
    • image input
    • watermark placement
    • watermark transparency
    • image output
  5. Having no control over the size, aspect ratio, brightness or contrast of the original/input images, the watermark needs to look good on a wide variety of image types without making the user fiddle with settings.

Getting Started

The basic design of this app requires three major functions to be addressed, given the requirements above:

  1. Present a UI to let the user specify an input directory, an output directory, and the text of the watermark. For simplicity, we'll just grab all of the JPEG images in the input directory using a loop over the array of filenames returned by a Directory object.
  2. Given the path to a JPEG file and a string representing the watermark we want on the image, we need to:
    1. Obtain a Bitmap object from the path and filename;
    2. Create a graphical representation of the watermark string which satisfies the requirements above...
      • a drop shadow would help provide better contrast over a variety of backgrounds.
      • a partially transparent watermark would allow placement within the image area.
      • the watermark should be sized appropriately to the image it's being applied to.
    3. Create an output Bitmap by applying the watermark to the source bitmap.
  3. Save the output Bitmap to a file in the output directory.

Step 1: Creating a rudimentary UI

Before we can process any images, we need some images to process (duh). Since this article isn't about file handling or Windows Forms, I'll recommend using code from another CodeProject article - Alberto Venditti's Image Batch Resizer - to get that part up and running. That project provides a shell that handles files in exactly the way we need - getting an input directory, an output directory, filtering the input files for JPEGs, and then looping though those input files to perform some bitmap processing on them. To get things moving along in a down-and-dirty fashion, just add a text box to frmMain for getting the desired text of the watermark from the user. As for the image processing itself, we'll need to make some changes to the btnGo_Click procedure in frmMain, specifically by calling a function we'll create in the next section in place of the call to the "Reduce" subroutine in Alberto's code. [Note - have a look at that code while you're in there, since it demonstrates some other very useful techniques, such as using memory streams to handle images, scaling bitmaps with minimal coding, and saving bitmaps and/or streams to files.]

Step 2: Basic watermarking of the image files

Since this was my first foray into making use of .NET's imaging features, I did what any good developer would do and Googled [VB.NET image watermark]. Jaison John's CodeProject article Watermark Website Images At Runtime was one of my first good finds. In addition to offering some nifty ASP.NET integration tricks, Jaison demonstrates how to accomplish a few things essential to our project. The core ideas I gleaned from Jaison John's main watermarking routine were:

  • Get a Bitmap object from a file on disk using one of the overloaded constructors of the .NET framework's Bitmap object. The Bitmap is the main .NET object representing an image, and the overloads we'll use to construct the object accepts a String argument of the path to an image file:
    Dim bmp as Bitmap = New Bitmap(strInputFilePath)
  • Get a Graphics object from a Bitmap using Graphics.FromImage(Bitmap). The Graphics class is a utility class that .NET provides for manipulating and modifying the Bitmap from which it was created.
  • Draw text onto the Bitmap using the .DrawString method of the Graphics class.

This was a great start. Jaison's technique draws 14-point Verdana text in Beige over a given image, starting at pixel (0,0), like so (code is a slightly-tweaked copy of Jaison's, comments are mine):

    'Construct a Bitmap object from a jpg's filename:
    dim bmp as Bitmap = New Bitmap(strInputFilePath)
    'Obtain a Graphics object from & for that Bitmap:
    dim canvas as Graphics = Graphics.FromImage(bmp)
    'Draw the watermark string onto the Bitmap:
    canvas.DrawString(strWatermark, _ 
        New Font("Verdana", 14, FontStyle.Bold), _
        New SolidBrush(Color.Beige), 0, 0)
    'Save the watermarked bitmap to a new file:
    bmp.Save(strOutputFilePath)

Not bad for such a short block of code, eh? Given our requirements, though, we still have some work ahead of us to whip our watermark into shape. [Note: the classes used in this article are found in the System.Drawing namespace, so save yourself some time and add the Imports statement for that namespace to your code module if you're singing along and starting from scratch.] This brings us to...

Step 3: Creating the Drop Shadow

A quick bit of trial and error revealed that successive calls to .DrawString would by default create text instances stacked one on top of another -- that is, the ZOrder of each bit of rendered text would increment with each successive call to .DrawString. Going back to the code block in the previous section, we can see that the .DrawString method of the Graphics object took five arguments:

  • the String to be drawn (strWatermark),
  • the font in which to draw it (a Font object in 14-point Verdana, styled in Bold),
  • a SolidBrush object containing the color in which to draw it (Color.Beige),
  • and the other two being the X and Y coordinates representing the top-left corner of the drawing within the Bitmap(0,0).

Armed with all that information, we have all that we need to change our code block like so to create our drop-shadow effect:

    'Draw the watermark string onto the Bitmap in Black to create
    'the "shadow", offset 2 pixels from our original position:
    canvas.DrawString(strWatermark, _ 
        New Font("Verdana", 14, FontStyle.Bold), _
        New SolidBrush(Color.Black), 2, 2)
    'Now given that a subsequent call to .DrawString will draw on
    '*top* of our previous text, we'll draw the watermark string onto 
    'the Bitmap again, this time in White, at the original position:
    canvas.DrawString(strWatermark, _ 
        New Font("Verdana", 14, FontStyle.Bold), _
        New SolidBrush(Color.White), 0, 0)

As you can see in the code comments, all we've done here is drawn the same bit of text - first in black, then in white - on top of itself with a 2-pixel offset. The end result of this is our white text with a 2-pixel black drop-shadow, which brings us to...

Step 4: Making the text transparent

There are many ways to skin a cat, and making text transparent in GDI+ is no exception. Just to extend my familiarity with the graphics functions in .NET, I tried three different approaches to this before settling on one. Briefly, these approaches were:

Sub-optimal approach #1: Using alternative brushes in the Graphics.DrawString method

The DrawString method of the Graphics class will accept any class derived from Brush for the third argument, and these include SolidBrush, TextureBrush, and HatchBrush. It didn't take too much fiddling around with these to find that they wouldn't provide a very direct solution to the transparency problem. These brushes can be quite useful for creating other effects, and in a pinch I could employ a hatch brush to achieve a faux-transparent effect, but this wasn't the obvious answer for the functionality I needed here.

Sub-optimal approach #2: The "Green-Screen" approach

An article on Vb-Helper.com presented another approach which involved:

  • creating a secondary Bitmap with just the text or watermark on it,
  • making the background color of this secondary Bitmap transparent,
  • looping through the secondary Bitmap in a pixel-by-pixel fashion, manipulating the transparency of the pixels by setting the ALPHA component to 128 (50% opacity), and finally
  • using Bitmap's .DrawImage method to draw the modified secondary Bitmap over the original picture.

This approach did exactly what I wanted. The only problems were that I was seeing some aliasing at the edges of the letters, and the performance of the pixel-by-pixel bitmap operation was clearly going to be unacceptable. However, the technique in that article did reveal some things about .NET's GDI classes that led me to my eventual solution. Let's look at that approach, specifically the portion of the code that manipulates the pixels' ALPHA components:

    ' Set the watermark's pixels' Alpha components.
    Const ALPHA As Byte = 128
    Dim clr As Color
    For py As Integer = 0 To watermark_bm.Height - 1
        For px As Integer = 0 To watermark_bm.Width - 1
            clr = watermark_bm.GetPixel(px, py)
            watermark_bm.SetPixel(px, py, _
                Color.FromArgb(ALPHA, clr.R, clr.G, clr.B))
        Next px
    Next py

The workers here are the Bitmap object's GetPixel and SetPixel methods, but the really interesting function is Color.FromArgb. This bit of code uses GetPixel to grab the values of each pixel in the image, then reduces the opacity of that same pixel by calling SetPixel and passing it a color argument constructed from:

  • an ALPHA component of 128 (or 50% opacity),
  • the original R, G and B values of the pixel.

That's where the light went on for me; if I'd have known earlier that a Color object could be constructed in a transparency-aware fashion, then I would have simply done that in the first place. Hindsight being 20/20, I went back to my original code...

The Solution: Skip all the BS and Just Draw Transparent Text in the First Place

Looking at the place where I was calling .DrawString and needed to pass a Color object to the SolidBrush object's constructor, it should be a no-brainer at this point to figure out what to change. Where I had previously created SolidBrush objects using Color.White and Color.Black, could now create semi-transparent brushes by constructing them with Color.FromArgb:

    'Draw the watermark string onto the Bitmap in Black to create
    'the "shadow", offset 2 pixels from our original position:
    canvas.DrawString(strWatermark, _ 
        New Font("Verdana", 14, FontStyle.Bold), _
        New SolidBrush(Color.FromArgb(128, 0, 0, 0)), 2, 2)
    'Now given that a subsequent call to .DrawString will draw on
    '*top* of our previous text, we'll draw the watermark string onto 
    'the Bitmap again, this time in White, at the original position:
    canvas.DrawString(strWatermark, _ 
        New Font("Verdana", 14, FontStyle.Bold), _
        New SolidBrush(Color.FromArgb(128, 255, 255, 255)), 0, 0)

There the problem was solved. The only changes needed to my earlier code are bolded. By constructing my SolidBrush objects using a semi-transparent color in the first place, I could completely avoid all the overhead of instantiating a second bitmap, not to mention all the repeated calls to GetPixel and SetPixel.

Step 5: Sizing the Watermark

So now all that's left is to address the matter of scaling our textual watermark to the image it's being applied to. The challenge here is to take a blob of text, which is vector-based and sized in points for a given font, and scale it relative to a bitmap, which is raster-based and sized by pixel count. We previously saw how to scale a bitmap fairly easily, but having just eliminated the need to instantiate a second bitmap, I wasn't too eager to go the route of putting the watermark on its own bitmap and scaling that to the JPG image without investigating some other approaches first. A bit of hunting around in the documentation turned up just what I was looking for; the Graphics class provides a MeasureString method that would return a SizeF structure containing the pixel dimensions of a given string drawn in a given font.

Armed with the MeasureString method, let's take stock of our variables and see what the most straightforward implementation would be. Our goal is to have our watermark "appropriately sized" for the JPG it's going on, so we have to decide what that means now. I consulted a psychic, an interior designer, my horoscope, and a well-known book on etiquette to determine that the ideal watermark size is 50% of the JPG's width. Your mileage may vary, and you should of course consult your own highly-respected sources. At any rate, we'll add variables for the SizeF structure, for the DesiredWidth, and for a Ratio we'll calculate, plus an object variable for the font so we don't have to keep creating a new Font object every time we reference it:.

dim StringSizeF as SizeF, _
    DesiredWidth as Single,
    wmFont as Font,
    RequiredFontSize as Single,
    Ratio as Single
wmFont = New Font("Verdana", 14, FontStyle.Bold)

Now we have everything we need to get some rough but per formant sizing logic up and running. First, we calculate the desired width of the watermark as 50% of the width of the JPG we'll be drawing it on (for simplicity's sake, I'm assuming the height will "just be okay," and in the vast majority of cases it is indeed.):

DesiredWidth = bmp.Width * .5

Next, we find out how big our string is, in pixels:

StringSizeF = canvas.MeasureString(strWatermark, wmFont)

Now that we know the size of our string when drawn, for example, in 14-point Verdana, we can derive a Ratio of [font size : pixel width] given our String and typeface. We'll do this by dividing the width of our String (in pixels) by the size of our font (in points):

Ratio = StringSizeF.Width / wmFont.SizeInPoints

Given that Ratio, it's just a simple extrapolation to get a font size in points that will yield a watermark of our desired width, given the same typeface and String:

RequiredFontSize = DesiredWidth / Ratio

Done! Now we simply reinitialize the Font, changing the hard-coded "14" to the RequiredFontSize variable, and we're ready to draw our watermark - transparent, drop-shadowed and sized for the image it's going on:

wmFont = New Font("Verdana", RequiredFontSize, FontStyle.Bold)
'Draw the watermark string onto the Bitmap in Black to create
'the "shadow", offset 2 pixels from our original position:
canvas.DrawString(strWatermark, _ 
    wmFont, _
    New SolidBrush(Color.FromArgb(128, 0, 0, 0)), 2, 2)
'Now given that a subsequent call to .DrawString will draw on
'*top* of our previous text, we'll draw the watermark string onto 
'the Bitmap again, this time in White, at the original position:
canvas.DrawString(strWatermark, _ 
    wmFont, _
    New SolidBrush(Color.FromArgb(128, 255, 255, 255)), 0, 0)
 bmp.SetResolution(96, 96)

And what exercise would be complete without a Gotcha! After running several folders worth of images through this algorithm, I saw some drastic variations in the size of the watermark that were seemingly unexplained. The debugger wasn't helping much here, so I sifted through the original images looking for anything that might be causing this. Sure enough, there were different DPI resolutions among the JPG files. D'oh! The right thing to do would be to get the DPI of the original JPG and handle the watermark accordingly, but it was getting late and this section of code was starting to become downright tiresome, so I cheated by adding a call to Bitmap.SetResolution(96,96) before all measuring and sizing logic. This had the intended effect of "normalizing" things so the watermark size was consistently 50% of the input image size again. Since there wasn't a noticeable performance penalty after adding this, I decided I was done. Oh, happy day!

Conclusion

After all our tweaks, if we string the above together we end up with a simple, straightforward and per formant function for watermarking images. Even though there's still plenty of room for customization and enhancement (such as letting the user pick the font, fiddle with the placement, color & transparency, etc.), we've fulfilled our design criteria at this point and can safely break for dinner. In my implementation, I did go back and add code to prepend a © copyright symbol to the watermark text. I also fiddled a bit with the ALPHA values, ultimately deciding that things looked a bit better if the white text was slightly more opaque (ALPHA = 136) than the black drop-shadow (ALPHA = 120).

Looking back on what it took to make this work, it's worth noting that all the functionality we needed was contained in a mere two classes in .NET - the Bitmap object and the Graphics object.

Happy watermarking!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Paul C Smith
Architect SWC
United States United States
Paul is a Software Architect at SWC Technology Partners, one of Crain’s 20 Best Places To Work for 2012. His coding exploits range from BASIC on a Commodore 64, but now focus on C#, SQL Server and the .NET stack. Paul also maintains the StormFactory ORM Code Generator on SourceForge.net. SWC Technology Partners is always looking for top technical talent; check out how cool it is to work here at http://reimagineyourcareer.com/, and feel free to contact me about positions here.

Comments and Discussions

 
GeneralMy vote of 5 PinmemberMember 1022520316-Sep-13 3:02 
QuestionPosition PinmemberASI-Pat3-Mar-13 11:21 
QuestionThanks a ton. You are a life saver. man. Pinmemberkaustubhd13-Feb-12 18:04 
Questionany way to remove the embedded watermark Pinmembergauravreads27-Apr-08 19:56 
QuestionSpool size gets bigger.. PinmemberJwalant Natvarlal Soneji29-Aug-07 0:11 
QuestionBut does it put itself on image PinmemberJwalant Natvarlal Soneji5-Aug-07 23:16 
GeneralThank You Pinmembera8le11-Mar-07 5:13 
GeneralThanks PinmemberJim Franklin10-Dec-06 6:35 
Generalif i change the pixel in the coding then the changes shd be in image PinmemberK.Gajalakshmi18-Aug-06 13:44 
QuestionSize of watermarked images! Pinmemberfatgeorge15-Jun-06 23:35 
AnswerRe: Size of watermarked images! PinmemberPaul C Smith16-Jun-06 6:42 
GeneralRe: Size of watermarked images! Pinmemberwayne113315-Apr-07 22:55 
GeneralRe: Size of watermarked images! PinmemberPaul C Smith16-Apr-07 7:18 
GeneralOkay so where is the function PinmemberTekbotics26-Mar-06 16:51 
GeneralCode Download Available PinmemberPaul Chu19-Mar-06 12:47 
GeneralGood article, small mistake Pinmemberdadinos20-Feb-06 3:00 
GeneralRe: Good article, small mistake PinmemberPaul C Smith14-Mar-06 6:41 
GeneralGood Article But....... Pinmember506603-Mar-05 10:33 
GeneralRe: Good Article But....... PinmemberPaul C Smith4-Mar-05 5:24 
GeneralRe: Good Article But....... Pinmember506604-Mar-05 5:56 
GeneralRe: Good Article But....... PinmemberMichael Proctor10-Mar-05 4:13 
GeneralWorth a 5 to newbies, good work :) PinmemberMichael Proctor14-Feb-05 23:28 
GeneralRe: Worth a 5 to newbies, good work :) PinmemberPaul C Smith16-Feb-05 13:23 
GeneralRe: Worth a 5 to newbies, good work :) PinmemberMichael Proctor18-Feb-05 0:16 
GeneralRe: Worth a 5 to newbies, good work :) PinmemberPaul C Smith18-Feb-05 8:32 
GeneralRe: Worth a 5 to newbies, good work :) PinmemberMichael Proctor19-Feb-05 23:56 
QuestionTypo? - Or am I missing something? Pinmemberisler-j1-Feb-05 7:37 
AnswerRe: Typo? - Or am I missing something? PinmemberPaul C Smith1-Feb-05 10:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.141015.1 | Last Updated 9 Feb 2005
Article Copyright 2005 by Paul C Smith
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid