Creating an Image Effect to put a play button on Video thumbnails

I had a rather interesting feature to implement on flocknote lately (after doing a pretty vast redesign of the UX/UI on the site over the past month... it was refreshing to dig into PHP again!):

We want to allow insertion of YouTube and Vimeo (and potentially other) videos into 'Notes' on the site, and there are a few moving parts in this equation:

  • I had to create a text format filter similar to the 'Embedded media inline' module in Drupal 6 so people could simply put a 'merge tag' in their Note (like [video=URL]) where they want the video to appear.
  • When a user views the embedded video on the site, the video should show at a uniform width/height, and be able to play the video (basically, a merge tag the user enters should be converted to the proper embed code for the provider (in this case, an <iframe> with the proper formatting).
  • When a user sees the video in the note email, the video can't actually play since very few email clients support any kind of video embedded in an email. So, instead, the video shows as a frame with a play button on top (this is the trickiest part), and links to the video on YouTube, Vimeo, etc.

Creating my own Image Effect for a Video Play Button

What I wanted to end up with was an image that had a custom-made iOS-style play button (play icon in a circle with a translucent grey background) right in the middle (I like the simple look of videos on my iPad...):

Video Play Button Example

So, I decided to work with Drupal's Image Effect API and expose a new image effect, aptly named 'Video Play Button', to Drupal's simple set of 'Resize, Scale, etc.' image effects. This is a pretty simple process:

  1. Implement hook_image_effect_info() to tell Drupal about the new effect.
  2. Process the image (in $image->resource) in the 'effect callback' that you defined in hook_image_effect_info().

In my case, I calculated the center of the image to be processed, then subtracted half the play button's width and height (respectively) from the center dimensions, and used those dimensions, along with the image handle ($image->resource) and the play button image (I used drupal_get_path() to get the path to my custom module directory, and put the image in 'images/play-button.png') to build the final graphic using PHP GD library's imagecopy() function.

Here's the image effect info hook implementation and callback I wrote to put the play button on top of the image:

<?php
/**
 * Implements hook_image_effect_info().
 */
function mymodule_image_effect_info() {
  return array(
   
'mymodule_video_play_button' => array(
     
'label' => t('Video Play Button'),
     
'help' => t('Adds a video play button in the middle of a given image.'),
     
'effect callback' => 'mymodule_video_play_button_callback',
     
'dimensions passthrough' => TRUE,
    ),
  );
}

/**
 * Video Play Button image callback.
 *
 * Adds a video play button on top of a given image.
 *
 * @param $image
 *   An image object returned by image_load().
 *
 * @return
 *   TRUE on success. FALSE on failure to colorize image.
 */
function mymodule_video_play_button_callback(&$image) {
 
// Make sure the imagecopymerge() function exists (in GD image library).
 
if (!function_exists('imagecopymerge')) {
   
watchdog('image', 'The image %image could not be processed because the imagecopymerge() function is not available in this PHP installation.', array('%file' => $image->source));
    return
FALSE;
  }

 
// Verify that Drupal is using the PHP GD library for image manipulations
  // since this effect depends on functions in the GD library.
 
if ($image->toolkit != 'gd') {
   
watchdog('image', 'Image processing failed on %path. Using non GD toolkit.', array('%path' => $image->source), WATCHDOG_ERROR);
    return
FALSE;
  }

 
// Calculate the proper coordinates for placing the play button in the middle.
 
$destination_x = ($image->info['width'] / 2) - 35;
 
$destination_y = ($image->info['height'] / 2) - 35;

 
// Load the play button image.
 
$play_button_image = imagecreatefrompng(drupal_get_path('module', 'mymodule') . '/images/play-button.png');
 
imagealphablending($play_button_image, TRUE); // Preserve transparency.
 
imagealphablending($image->resource, TRUE); // Preserve transparency.

  // Use imagecopy() to place the play button over the image.
 
imagecopy(
   
$image->resource, // Destination image.
   
$play_button_image, // Source image.
   
$destination_x, // Destination x coordinate.
   
$destination_y, // Destination y coordinate.
   
0, // Source x coordinate.
   
0, // Source y coordinate.
   
70, // Source width.
   
70 // Source height.
 
);

  return
TRUE;
}
?>

...and a PSD of the play button is attached, in case someone else wants to save themselves 10 minutes' drawing in Photoshop :)

There's another great example image effect, if you want to look at more examples, in the Examples for Developers modules' image_example.module.

imagecopy() vs. imagecopymerge()

...and Photoshop Save for Web vs. PNGOut optimization...

I spent almost an hour working on a couple different problems I encountered caused partly by the fact that I was using a compressed/optimized PNG file, and partly by the fact that I was misreading the PHP.net documentation for two GD library image copy functions, imagecopy() and imagecopymerge().

First of all, instead of spending a ton of time struggling with weird file dimension issues, transparency issues, etc., and thinking your code is causing the problem—even though it may—also try different image files or try exporting the image file you're manipulating/using a different way. In my case, the image I was using was run through PNGout to remove any extraneous data, but apparently too much data was removed for PHP's GD library to understand the file correctly—in my case, the file's dimensions were distorted, the alpha transparency was not respected, and the image had lines of interpolation... all because I had tried to use an optimized PNG instead of the direct 'Save for Web...' image from Photoshop.

With regard to GD image functions, imagecopy() allows you to put one image on top of another one, hopefully preserving alpha transparency, etc., while imagecopymerge() puts an image on top of the other without preserving alpha transparency, but while allowing you to set the opacity of the source image manually (from 0-100%). I was originally trying to get imagecopymerge() to put a circle 'play' button (iOS-style) on top of the video image, but I found that the function was putting a square frame with a grey background instead of the nice transparent area around the circle. Switching to imagecopy() seemed to preserve the 24-bit PNG alpha transparency better.

This bug report on php.net was especially enlightening when I was researching why imagecopymerge() wasn't working for me.

Conclusion

There are a few other moving parts to this equation, like retrieving the YouTube or Vimeo video frames, building the proper markup for different displays (on-site, email, mobile, etc.), etc., that I haven't gone into here, but I figured I'd share my experience creating a custom image effect here in case someone else wants to do something similar (like put watermarks on images for a photo site, or something like that).

Comments

You could have used the imagecache_actions module too, it is very complete. Always interesting to see some code though.

I remember using imagecache_actions in the past for some sites; on this particular site, I already have a few custom modules doing specific tasks (like the play button), so it's a better fit to not have the overhead of another module. But for many sites, imagecache_actions would definitely be the way to go.

As Jeff mentioned, the best route for this is ImageCache Actions module. You can make this completely automated so that a play button will overlay preview images in things like a Views listing. There's a great tutorial to adding play button overlays with ImageCache Actions to do just that if anyone is looking for a modern Drupal 7 approach.