RMagick Watermarking Images With the shade Method

Looking for a creative way to watermark the images on your web site? Trying to keep people from using your photographs without permission? Notice that stock photography sites like iStockphoto add their site name to their sample images using semi-transparent text. This article shows how to use RMagick's shade method and the HardLightCompositeOp composite operation to add a digital watermark that looks like embossed transparent text.

Here's the image we're going to produce. The text "Watermark by RMagick" appears to be embossed on the image. Notice that the watermark is mostly transparent so it doesn't entirely obscure the image, but at the same time it prevents the use of the image without the watermark. In a real application you might use this technique to add "Copyright © by John Smith" to your own photos.

Watermarked image

As usual, this is not an introductory tutorial to RMagick. I'm assuming that you are already somewhat familar with routine uses of RMagick, such as how to read and write image files. If you've been reading these articles as I've been adding them, then you've already learned how to use all but one of the methods I'll be using in this article. If you haven't already read part 1 of my Alpha Compositing articles, then go read it and come back. We'll wait.

Here's the script. There's just over two dozen lines of code. After this listing, I'll go through the script and explain each part.

require 'RMagick'

if !ARGV[0]
    puts "Usage: watermark <path-to-image>"
    exit
end

image = Magick::Image.read(ARGV[0]).first

mark = Magick::Image.new(image.columns, image.rows)

gc = Magick::Draw.new
gc.gravity = Magick::CenterGravity
gc.pointsize = 32
gc.font_family = "Helvetica"
gc.font_weight = Magick::BoldWeight
gc.stroke = 'none'
gc.annotate(mark, 0, 0, 0, 0, "Watermark\nby\nRMagick")

mark = mark.shade(true, 310, 30)

image.composite!(mark, Magick::CenterGravity, Magick::HardLightCompositeOp)

out = ARGV[0].sub(/\./, "-wm.")
puts "Writing #{out}"
image.write(out)

Read the image to be watermarked

require 'RMagick'

if !ARGV[0]
    puts "Usage: watermark <path-to-image>"
    exit
end

image = Magick::Image.read(ARGV[0]).first

mark = Magick::Image.new(image.columns, image.rows)

The script expects the name of the image to be watermarked as its only argument. We start making the watermark by creating a solid white image the same size as the input image. Strictly speaking, the watermark image doesn't have to be the same size, just big enough to contain the watermark and small enough to fit on the image.

Draw the watermark

gc = Magick::Draw.new
gc.gravity = Magick::CenterGravity
gc.pointsize = 32
gc.font_family = "Helvetica"
gc.font_weight = Magick::BoldWeight
gc.stroke = 'none'
gc.annotate(mark, 0, 0, 0, 0, "Watermark\nby\nRMagick")

Use Magick::Draw#annotate to write solid black text on the white background. When we use annotate we can use annotate attribute methods to control the styling of the text. Here we use the font_weight, pointsize, and font_family attributes to specify a bold 32-point Helvetica font. Normally text is both filled and stroked, but we remove the stroke by specifying the stroke as "none". We don't need to specify a fill because the default is "black." We control the position the text with gravity. CenterGravity positions the text in the center of the image. Since the image has multiple lines, each line is centered. Here's the result. I've put a border around it so we can see the dimensions, although actually the image has no border.

watermark text

Shade the text

Right now the text is simply flat and black. We want to make it look raised and lit by a strong light source. This is a job for the shade method. According to the RMagick documentation, shade "shines a distant light on an image to create a three-dimensional effect." The shade method takes 3 arguments, shading, azimuth, and elevation.

The shading argument can be either true or false. If shading is true then the shading is done on the intensity of each pixel, so the resulting image contains only shades of gray. I'm sure that false is useful but I don't know what for. I always use true.

Azimuth, the second argument, specifies the direction from which the light shines, measured in degrees. 0° is 9 o'clock (west). Increasing values of azimuth move the light source counter-clockwise, so 90° is 6 o'clock (south), 180° is 3 o'clock (east) and 270° is 12 o'clock (north).

Elevation, the third argument, specifies the position of the light source above the land. The land is the part of the image that is not the edges of the letters, that is, the background of the image and the interior portions of the letters. Here's a diagram for elevation:

elevation diagram

In order to understand how azimuth and elevation work, I found it useful to create an image that demonstrated the effect of various combinations of values for these two arguments. Each pair of numbers is a combination of "azimuth, elevation", where each row has the same value of azimuth and each column has the same value of elevation. I call this my "shade examiner." Here's a small rectangle from the image:

shade examiner sample

I'd show you the whole image, but it's a 640K download, so in deference to your bandwidth I'll give it to you in compressed form. Here it is in 658 bytes:

#!/usr/bin/env ruby
require 'RMagick'

mark = Magick::Image.new(125, 55)

ilist = Magick::ImageList.new

gc = Magick::Draw.new
gc.gravity = Magick::CenterGravity
gc.pointsize = 32
gc.font_family = "Times"
gc.font_weight = Magick::BoldWeight
gc.stroke = 'none'

0.step(360, 10) do |azimuth|
    0.step(180, 15) do |elev|
        frame = mark.copy
        gc.annotate(frame, 0, 0, 0, 0, "#{azimuth}, #{elev}")
        ilist << frame.shade(true, azimuth, elev)
        puts "Adding #{azimuth}, #{elev}"
    end
end
puts "Montaging..."
montage = ilist.montage do
    self.geometry = "125x55+0+0"
    self.tile = "13x36"
end

montage.write('shade_examiner.gif')

This program will, when executed, produce a complete 1625x1980-pixel version of the shade examiner. Go ahead and run it now, then use ImageMagick's display command (or, if you're running on MS Windows, some other graphics viewer such as Internet Explorer) to view the shade_examiner.gif file. Notice that the elevation argument changes the color of the land. When the elevation is 0° the land is black. When the elevation is 90° it's white. When the elevation is 30° the land is 50% gray, exactly halfway between black and white. This is an important behavior for our purpose. I'll explain why later.

Okay, let's get back to the watermarking program. Call shade:

mark = mark.shade(true, 310, 30)

We can choose any value we like for the azimuth. Here I've chosen 310°, which places the apparent light source in the northwest. Notice I've used 30° for the elevation. This is a critical number for this program! Here's the output from shade.

watermark after shading

The shade method has changed the flat black text on white background into gray text on a gray background. The entire image is 50% gray except for the edges of the text, which are white in the direction of the apparent light source and black in the opposite direction, giving the appearence of raised text.

Composite the watermark onto the image

image.composite!(mark, Magick::CenterGravity, Magick::HardLightCompositeOp)

Now that we have a watermark, how do we add it to the image? If you've been keeping up with these articles you might guess that we're going to do some sort of compositing. The question is, which composite operator should we use? If we scan through the list of CompositeOperator constants in the RMagick doc we come across this one:

HardLightCompositeOp
Multiplies or screens the colors, dependent on the source color value. If the source color is lighter than 0.5, the destination is lightened as if it were screened. If the source color is darker than 0.5, the destination is darkened, as if it were multiplied. The degree of lightening or darkening is proportional to the difference between the source color and 0.5. If it is equal to 0.5 the destination is unchanged. Painting with pure black or white produces black or white.

This sounds promising. We want to lighten the image where the watermark is lighter than the land and darken it where the watermark is darker than the land. Where the watermark is the land color we want the image to be unchanged. According to the description, the destination image (the photograph) is unchanged when the source (the watermark) color is 0.5. What is 0.5? It turns out that 0.5 means 50% gray.

Remember that when shade's elevation argument is 30° the land is 50% gray. By using 30° as the elevation we've made the watermark land be 50% gray. Therefore, after composting the portions of the image that correspond with the land are unchanged. Where the watermark is white the image is white and where the watermark is black the image is black. Where the watermark is neither black nor white, and also not exactly 50% gray, the image is lightened or darkened depending on the corresponding shade of gray in the watermark. If we magnify a portion of the watermarked image we can see the effects of different shades of gray.

effect of hard light composite operator

The result of the composite operation is our watermarked image. All that's left to do is append "-wm" to the image filename and write the image to disk.

out = ARGV[0].sub(/\./, "-wm.")
puts "Writing #{out}"
image.write(out)

Conclusion

It took a long time to explain shade but I think it was worth it. For me the uses of this method has always been a bit obscure so I'm pleased to be able to explain one of them. You can find more uses of shade in the "Advanced Techniques" section of Anthony Thyssen's excellent Examples of ImageMagick Usage site.

Watermarked image

Tim - Nov 13, 2006