![]() |
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.
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)
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.
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.
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:
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:
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.
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.
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:
HardLightCompositeOpThis 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.
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)
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.
Tim - Nov 13, 2006