A common response when someone asks, "Can Drupal do that?" is almost always a resounding yes. However, when a recent project required that Drupal merge CMS capabilities with Photoshop-like functionality, I began to question if Drupal really was the right solution.
ImageCache offers some pretty powerful features, but it is no Photoshop. So where ImageCache leaves off, Imagemagick (aided by Imagick) steps in. Together, these tools magically turn Drupal into Photoshop... well, sort of.
First, let me back up and explain why we needed Drupal and Photoshop merged together. The goal was to display content that would float across two large touch screens located in the lobby of a museum. Users could interact with the floating objects by tapping on them to learn more about the selected item. Additionally, users could scan QR codes that were positioned around the touch screens to learn more about the content they were seeing. The client also needed an easy way to create and update the content provided to the software powering the screens. Much of the text needed to be rasterized to keep CPU utilization down and framerate up, while also needing to reflect changes to existing content and include new content. Thus Drupal became the mechanism for facilitating content entry and formatting, displaying content over the web when QR codes were scanned, and supplying content to the touch screen application.
Let's begin by explaining what Imagemagick and Imagick do, as well as the differences between the two. Imagemagick is a command line utility for manipulating images. With the right combinations of arguments and flags, pretty much anything is possible. Think of it as a command line interface for Photoshop. However, what if the command line isn't your thing or you want to keep all of your code in one language? Now I know we can execute command line statements through PHP, but what fun is that when you really want to just stick with PHP, since after all we are using Drupal. This is where Imagick comes in. Imagick is an object oriented PHP class for creating, manipulating, and saving images using the Imagemagick library. This ease of use comes with a slight penalty though. Since most online examples use Imagemagick only, you will have to do considerably more hunting for detailed Imagick examples. This guide by no means intends to demonstrate advanced Imagick features, but I hope to at least give you a basic working knowledge for future exploration and a framework for integrating it with Drupal, which is more than enough to make you dangerous.
If this all sounds too good to be true, it isn't. You do, however, need to start by downloading and installing the Imagemagick and Imagick libraries and get them working in your dev environment. There are a plethora resources out there on this topic, and I couldn't even begin to explain the intricacies of installing this for all of the different dev environments. So I am leaving you on your own for this part of the walkthrough, but I will point you to several of the resources and tips I found most helpful. Good luck!
- Verify that the Xcode command line tools are installed as that seems to be a common requirement among many of the various installation techniques.
- There are several package managers that support Imagemagick installation, but the Homebrew package manager is one of my favorites.
$ sudo brew install imagemagick
You can then verify the success of the installation with this command.$ convert -version
Now that you have Imagemagick and Imagick installed, let's say you want to create a new image.
/*
* Create new Imagick object.
*/
$image = new Imagick();
That was simple enough, now what if you want to create an Imagick object from an existing image?
/*
* Create Imagick object from existing image.
*/
$background_image = 'block.png';
$image = new Imagick(drupal_get_path('module', 'imagick') . '/' . $background_image);
Now let's say you need to know the dimensions of the image, so you can crop the image if it is over a certain size.
/*
* Find the dimensions of an image.
*/
$image_width = $image->getImageWidth();
$image_height = $image->getImageHeight();
There would be some if logic in here, but regardless this is how you crop an image. To make it a bit more dynamic, let's crop our image based upon some values set on the node.
/*
* Crop an image.
*/
$image_width = $node->field_width[LANGUAGE_NONE][0]['value'];
$image_height = $node->field_height[LANGUAGE_NONE][0]['value'];
$image->cropThumbnailImage($image_width, $image_height);
Still not impressed? You say, "ImageCache can do all of that so far!" Well, hopefully you are beginning to see the ease at which you can apply conditional logic based upon node field values, and how the node object quickly becomes a settings palate ripe for generating all sorts of custom image iterations.
Now comes the real "magick" (sorry). When was the last time you wrote text to an image with ImageCache and applied a shadow to the text? Never? That's what I thought!
First, we are going to set some initial conditions. I found the more you separate out your settings from the implementation, the easier it is to work with when you invariably get requests for changes. Obviously this isn't a new revelation when it comes to programming, but I found it especially important when working with Imagick.
$text_size = 20;
$padding = 30;
$shadow_color = '#000';
$text_color = '#FFF';
$text_string = $node->body[LANGUAGE_NONE][0]['value'];
$text_width = $image_width - $padding * 2;
Now that the initial conditions are set, we can begin creating the ImagicDraw object that we will use to write text to our Imagick object.
$font = new ImagickDraw();
$font->setFont('Helvetica');
$font->setFontSize($text_size);
$font->setFillColor($text_color);
This short line hides enormous functionality behind it. It takes an Imagick object and an ImagickDraw object, along with the text string and the width of the desired text area, and breaks the text into lines that fit within the size constraints. It then returns an array of the lines of text, with each line fitting within boundaries of the box. This highly useful function was found on StackOverflow, so vote up the answer if you get the chance. I included a slightly modified version of this function in my source files that are included at the bottom of this post. Whether you use my version or grab the original, just make sure you define the function before you start trying to call it.
// Break the text into separate lines.
list($lines, $lineHeight) = wordWrapAnnotation($image, $font, $text_string, $text_width);
Here we calculate the height of our text area, so that later we can center it vertically inside of our image.
// Vertically align the text.
$text_area = $image_height - $padding * 2;
// Calculate height of wrapped text.
$text_height = count($lines) * $lineHeight;
// Calculate offset based upon text_area and text_height.
$offset = round(($text_area - $text_height) / 2);
// Account for the y offset of the padding, and also account for imperfections in line height calculation.
$text_y = $padding + $offset * .9;
// Set the x offset of the text.
$text_x = $padding;
Now that we have everything set up to be offset, centered, and aligned properly, we can finally begin writing the text to the Imagick object. I begin by creating a large blank image, which I then crop to the dimensions of the text that were obtained above. Finally, I iterate through each line in the text array as returned by wordWrapAnnotation, offsetting each line by a consistent offset determined by the variable above.
// Create a new text object for writing text to.
$text = new Imagick(drupal_get_path('module', 'imagick') . '/' . 'background_image.png');
$text->cropThumbnailImage($text_width, $text_height);
// Write the text to the image.
for($i = 0; $i < count($lines); $i++) {
$text->annotateImage($font, 0, $lineHeight *.8 + $i*$lineHeight, 0, $lines[$i]);
}
Now for the moment you have all been waiting for: the application of the shadow. We begin by cloning the $text Imagick object we created above, and then use the setImageBackgroundColor to set the color of our shadow. We then call shadowImage, where the first parameter is the opacity of the shadow, which is a float value between zero and one hundred, and the second paramter is the sigma, which is a float value that sets the blur of shadow. The last two parameters are supposed to control the x and y offset, but I couldn't get them to have any effect on the shadow, no matter what values I threw at them.
/*
* Apply a text shadow.
*/
// Create shadow text overlay image.
$shadow = $text->clone();
// Set the shadow color.
$shadow->setImageBackgroundColor(new ImagickPixel($shadow_color));
// Apply a shadow to the layer.
$shadow->shadowImage(30, 5, 4, 4);
Combining the images again is really quite simple. The first parameter of compositeImage is the image you wish to composite, while the second parameter is a composite operator. Finally, the last two parameters are the x and y offsets that determine the placement of the composited image. So with a few calls to compositeImage, we now have a finished image.
// Put the two text images together.
$shadow->compositeImage($text, Imagick::COMPOSITE_OVER, 0, 0);
// Add the text to the image.
$image->compositeImage($shadow, Imagick::COMPOSITE_OVER, $text_x, $text_y);
Now for the most important part: actually saving the image and making it available to Drupal. Saving is relatively easy, but there are some pitfalls to be aware of. If you simply overwrite an existing image without changing the filename, ImageCache will not realize there is a new image, and thus will not update the thumbnail on the node edit page. I worked around this by simply appending the value of time('U') to my file name. However, calling, image_style_flush($style) would probably work too, if you don't mind regenerating all of your images for a given ImageCache preset.
/*
* Save the image.
*/
// Set image format.
$image->setImageFormat("png");
// Set image compression quality.
$image->setCompressionQuality(100);
// Set the destination.
$filename = sprintf('%d-%d.png', $node->nid, date('U'));
$destination = sprintf("%s/%s/%s", variable_get('file_public_path', conf_path() . '/files') , $node->type, $filename);
// Save the image out to a file.
$image->writeImage($destination);
// Update the database to let it know about our new file.
$data = file_get_contents($destination);
$file = file_save_data($data, sprintf('public://%s/%s', $node->type, $filename) , FILE_EXISTS_REPLACE);
// Attach the file object to the node.
$node->field_image[LANGUAGE_NONE][0] = (array)$file;
So now you have your image generation scripts created and let us suppose you have a healthy number of nodes of your new content type. When you change an ImageCache preset, Drupal automatically takes care of flushing out old images and regenerating new ones for you, but Drupal has no mechanism for regenerating all of your Imagick images when you change something in your Imagick code. In my example module attached below, I have tied the regeneration of the image on a node to the hook_node_presave function. However, you obviously don't want to have to edit and save each node on your site each time you need to make an adjustment, so we just need to find a way to trigger a save operation on multiple nodes at once. In comes our friend Views Bulk Operations. Set up a VBO View and include all of your desired node types, and then enable the Save post (node_save_action) action for your View. You should now be able to easily regenerate images sitewide, but just make sure to check your PHP max_execution_time and memory_limit variables before you start a large batch process.
Now with all of this code-controlled image generation going on, it is easy to let your files directory get out of control, so be sure that your scripts are deleting the old images from the file system and the database before you have a mess on your hands. The code below should take care of this for you by keeping your database up to date and your files directory clean.
// Delete the existing file.
if (isset($node->field_image[LANGUAGE_NONE][0]['fid'])) {
$file = file_load($node->field_image[LANGUAGE_NONE][0]['fid']);
file_delete($file, TRUE);
}
Finally, with the push towards removing images and replacing them with icon fonts or CSS3, there may be reduced application for the use of Imagemagick within a traditional Drupal site. However, perhaps you want to use Drupal as a CMS and/or API for managing content that will be displayed on some other medium, as I was for this project. This is just one potential use case, and I am sure some of you have used Imagick before, or now have ideas on how you could use it in your next project. So comment away and let us know how you have used Imagick/Imagemagick on previous projects or how you plan to use it on future projects.
Also, be sure to download the source files, which are actually a basic Drupal module I created that will get you up and running in no time. All you need is a standard Drupal 7 site with a node type that has a field with the machine name of field_image along with Imagick and Imagemagick successfully installed, and you should be set.