Giving Text an Inner Shadow with ImageMagick and Perl
Creating a CGI script that composites text with fancy effects onto an existing image is easier than you think
My memoir, The Accidental Terrorist, is about my youthful misadventures as a Mormon missionary. Missionaries always wear black name tags, so to promote my book I thought it would be nice to give fans a way to create and share their own customized name tag images.
To accomplish this, I figured a simple CGI script written in Perl would be best. I had a vague sense that I could use the Perl interface to ImageMagick to overlay a name in bold white text onto a blank name tag image like this one:
What’s more, I wanted the name to look like it had actually been stamped or drilled into the name tag, with maybe a slightly pebbled white surface to give things a nice feeling of texture.
I had used ImageMagick before for some simple applications, and I knew it was a very powerful graphics-processing package. However, it’s also very arcane, without much in the way of user-friendly documentation. (Oh, there’s plenty of documentation. It just helps to be fluent already in graphics-processing-ese to understand it.) Stack Overflow, to name just one forum, overflows with questions about how to do this or that with ImageMagick.
I scoured the web for an answer to what I thought was my very simple question about how to make an inner shadow, but I came up empty. Finally, all I could do was start playing around until I figured it out for myself.
I did figure it out, and I’ll lay out my method below in case there’s anyone else out there looking for an answer to the same question. I’m not claiming this is the best solution, in fact, I’m sure there’s probably some fiendishly clever way to do this in ImageMagick with a single convoluted command. Me, though, I like to take things step-by-step so I can easily see what’s happening at every point and why.
Having said that, my method is pretty straightforward, though a few of the details are a little tricky. We’ll start with the declarations, initializing a bunch of variables we’ll need later (some of which we can futz around with to adjust our output):
That all should be pretty self-explanatory, though we’ll talk more about some of these variables below.
Next, we declare a couple of
Image::Magick objects and load them with, respectively, the blank name tag graphic from above and the pebbled texture graphic below:
So far, so good. But before we actually try to print any text on either of these images, we need to gather some information about the text itself — specifically, how wide it will be when rendered:
QueryFontMetrics is a method of Image::Magick that, when passed some text descriptors, returns an array of stats about how that text will be rendered. The only return value we're interested in for our purposes here is
$width, which will help us center the text properly.
$starty describe the point on the name tag around which we'll center the text. Knowing the width of the text, we can easily calculate where the upper left corner will need to fall:
If we wanted to center the text vertically as well, we could calculate that from the
$height value, but in this case, we only need to know where the top edge of the text will fall.
Now we start getting to the interesting stuff. Our next step is to construct a mask, which is a grayscale image used as a filter when compositing one image onto another. The black parts of a mask will render the composited layer transparent, while the white parts will render it opaque. The levels of gray in between provides varying degrees of opacity.
I find it a little difficult to think of masks in those terms, though. It might be simpler to think of a mask as a stencil. You can lay your stencil down on the base layer of the image you want to composite, then sort of “spray-paint” your top layer through it.
You’ll see what I mean after a couple more steps. For now, we’re going to create our mask image by initializing a new
Image::Magick object, filling it with black, and then printing our (properly positioned) text on it in white:
The chunk of code above results in the following image:
See, doesn’t that look like a stencil? We’ll be using this mask in our final step to spray bits of one image onto another while blocking out other bits.
Okay, now we’re going to construct our shadow. This is what we’ll eventually composite with our text layer to give our name tag the 3-D look that we want. To create this shadow, we need to construct a new image that looks a lot like a mask but really isn’t.
The process is very similar to making our mask above. We want our shadow to be shaped like our text, so we again build an image with white text on a black background (though we could just as easily use a brown or purple background, or anything else we feel like):
But this time we do two things differently. We offset the text a little, in this case moving it down vertically by two pixels. Then we apply a Gaussian blur effect to the image, using a couple of variables that affect the degree to which the image gets blurred (play around with those values to see what happens). This gives us the following result:
Like I said, while this looks very similar to our mask image, it’s not exactly the same sort of thing. What we’re going to do with it — and this is where the magic really starts to happen — is layer a translucent version of it on top of our texture image. The code to do this is very simple:
And that gives us the following image:
We now have a composite image that looks like bright fuzzy letters projected onto a pebbled charcoal wall. The fact that the texture is only faintly visible is the result of our
$opacity parameter, which we could easily dial up or down, depending on the effect we wanted.
Now we’re ready for the final step. We take that stencil from way back and spray our composite shadow layer through it onto our original blank name tag:
We write the result to the file system, and voilà! Here’s our final image, looking quite fine:
There’s no doubt a way to do this in fewer steps, but what we have here was certainly acceptable for my purposes and not all that difficult.
If you try this code out with your own images, I’d suggest spending time playing around with the values of the initial parameters, and with different colors for the shadow layer. You might be surprised what you end up with!
In the end, my script was a little more complicated than what I’ve presented here, giving users a way to input a name and also choose from different image sizes with various slogans. But the code above is where the magic (or rather, the Magick!) all happens.