Templated social sharing images using WordPress, GD image, transparent PNGs, and magic.

I’m in the process of building a WordPress site where all the images that I intend to upload will be transparent PNGs. I also plan to post links to each post via social media and wanted to automate to process of creating the various images varieties needed for different networks as part of the standard WordPress upload process.

I didn’t want to simply use a cropped/resized version of the uploaded image (which would work fine for photos, but I’m not uploading photos), instead I wanted to apply the transparent PNG over a template. And while I’m at it, I figured I could use the colour info I was extracting from the uploaded image to further customise the template image itself.

process-example

Step 1: What sizes do I need?

This probably is the easiest part of the whole process, adding additional image sizes in WordPress is super simple, just specify them in your functions.php file and you’re good to go.

Each image size needs a unique name, width, height, and a crop directive. You can read more about the specifics in the WordPress codex.

For my purposes I needed four specific sizes to cater for four specific social networks: Twitter, Facebook, Instagram and Dribbble. I also settled, after numerous tests, on a default center/center position for any cropping - however I will be overriding this depending on the image aspect ratio.

Facebook - 1200 x 630

<?php add_image_size( 'facebook-image', 1200, 630, array('center','center') ); ?>

Dribbble - 800 x 600

<?php add_image_size( 'dribbble-image', 800, 600, array('center','center') ); ?>

Instagram - 640 x 640

<?php add_image_size( 'instagram-image', 640, 640, array('center','center') ); ?>

Twitter - 640 x 400

<?php add_image_size( 'twitter-image', 640, 400, array('center','center') ); ?>

Step 2: Altering image crop based on aspect ratio

The image that I will be uploading fall into three categories: tall portrait aspect, square (or close to it), width landscape aspect.

The filter I’ve used is based on the example given in the WordPress codex tweaked to apply the override based on specific conditions - in this case, aspect ratio, and “destination” dimensions (ie. the image variant being created).

The end goal for this filter are to have:

  • the tall portrait aspect images cropped from the top left (ie. if it were person, you would get their head and shoulders rather than their waist)
  • the square, and wide landscape aspect, image resized to fit the bounds of the 800x600 Dribbble image (still undecided if I also apply this rule to the other social variants)
  • and for everything else to just continue on with whatever default setting that had applied.
<?php

add_filter( 'image_resize_dimensions', 'conditional_image_resize_dimensions', 10, 6 );

function conditional_image_resize_dimensions( $payload, $orig_w, $orig_h, $dest_w, $dest_h, $crop ){

    $orig_aspect = $orig_h/$orig_w;
    if($crop && $orig_aspect > 1.5) {

        $aspect_ratio = $orig_w / $orig_h;
        $new_w = min($dest_w, $orig_w);
        $new_h = min($dest_h, $orig_h);

        if ( !$new_w ) {
            $new_w = intval($new_h * $aspect_ratio);
        }

        if ( !$new_h ) {
            $new_h = intval($new_w / $aspect_ratio);
        }

        $size_ratio = max($new_w / $orig_w, $new_h / $orig_h);

        $crop_w = round($new_w / $size_ratio);
        $crop_h = round($new_h / $size_ratio);

        $s_x = 0;
        $s_y = 0;

        // the return array matches the parameters to imagecopyresampled()
        // int dst_x, int dst_y, int src_x, int src_y, int dst_w, int dst_h, int src_w, int src_h
        return array( 0, 0, (int) $s_x, (int) $s_y, (int) $new_w, (int) $new_h, (int) $crop_w, (int) $crop_h );

    } else if($crop && $dest_w === 800 && $dest_h === 600) { // Dribbble

        $crop_w = $orig_w;
        $crop_h = $orig_h;

        $s_x = 0;
        $s_y = 0;

        list( $new_w, $new_h ) = wp_constrain_dimensions( $orig_w, $orig_h, $dest_w, $dest_h );

        return array( 0, 0, (int) $s_x, (int) $s_y, (int) $new_w, (int) $new_h, (int) $crop_w, (int) $crop_h );

    } else {
        return $payload;
    }

}

?>

Step 3: Retrieve the most prominent colour from the uploaded image

The retrieval of the most prominent colour is something that I was already doing - as I’m using this colour for some basic custom per-post styles within my WordPress theme - so I thought I’d make further use of the retrieved colour by using it to customise the image template too.

I’m using a php class that I found on GitHub to pull the colours  out of my uploaded image, you can find the colorsofimage.class.php here.

I’ve wrapped this class call up inside a simple WordPress filter that I’ve attached to the hooks tied to the generation and updating of attachment metadata - because that’s where I’m storing the retrieve colour.

<?php

function my_image_filter($metadata,$id) {

    $colors_of_image = new ColorsOfImage( site_url().'/wp-content/uploads/'.$metadata['file'] , 10 , 1 );
    $metadata['colours'] = $colors_of_image->getProminentColors();

    add_social_variant($metadata, $id, 'Facebook');
    add_social_variant($metadata, $id, 'instagram');
    add_social_variant($metadata, $id, 'twitter');
    add_social_variant($metadata, $id, 'dribbble');

    return $metadata;

}

add_filter( 'wp_generate_attachment_metadata', 'my_image_filter', 10, 2 );
add_filter( 'wp_update_attachment_metadata', 'my_image_filter', 10, 2 );

?>

Step 4: Generate the social image variants

This is the fun part.

WordPress has already created all the correctly cropped and/or resized versions of my images (steps 1 and 2), now I need to build the social image by combining the uploaded image, with a pre-uploaded template, and the prominent colour.

If you look back in step 3 you’ll see I’ve included the function calls for generating the social variants as part of the filter I’ve already applied. This is mostly because I needed to make sure that this step ran after the colour was retrieve. Bundling it into the existing filter was simplest.

My add_social_variant function accepts three basic parameters, the metadata ($meta) of the image that’s just been uploaded, the numerical id ($img) of the uploaded image, and the relevant social network ($type).

For the moment there is a simple check to see if the particular social variant already exists, and to only run the rest of the process if it doesn’t. I might go back and finesse this at some point, but for the moment I only need to worry about the initial generation of these images.

What this function achieves is creating a solid colour background (using the prominent image colour from the previous step), applying the template image (which has transparent section to allow the base colour to show through), and then adding the appropriately sized version of the uploaded image over the top.

All this is taken care of the by GD image functions available in PHP.

The generated image gets saved into a new ‘social-image’ directory that I’ve setup within the main WordPress uploads directory.

It was a conscious decision to not save these social images into the same year/month location as the rest of the image variants, I may change my mind on that in the future, but for now that’s what works for me.

<?php

function add_social_variant($meta=null, $img=null, $type=null) {

    if(!empty($meta) && !empty($img) && is_numeric($img) && !empty($type) && in_array($type,array('instagram','Facebook','twitter','dribbble'))) {

        $upload_dir = wp_upload_dir();

        $file_path = $upload_dir['basedir'].'/social-image/'.$type.'-'.$img.'.png';

        if(!file_exists($file_path)) {

            $p = explode('/',$meta['file'],-1);

            $social_image_url = $upload_dir['baseurl'].'/'.join('/',$p).'/'.$meta['sizes'][$type.'-image']['file'];

            if($type === 'dribbble') {
                $social_image_width = 800;
                $social_image_height = 600;
            } else {
                $social_image_width = $meta['sizes'][$type.'-image']['width'];
                $social_image_height = $meta['sizes'][$type.'-image']['height'];
            }

            $rgb = xHexToRGB($meta['colors'][0]);

            $base = imagecreatetruecolor($social_image_width,$social_image_height);
            $base_color = imagecolorallocate($base, $rgb[0], $rgb[1], $rgb[2]);
            imagefill($base, 0, 0, $base_color);
            $bg = imagecreatefrompng(get_stylesheet_directory_uri().'/resources/'.$type.'-bg-template.png');

            imagecopymerge_alpha($base, $bg, 0, 0, 0, 0, $social_image_width,$social_image_height,100);
            imagedestroy($bg);

            $img = imagecreatefrompng($social_image_url);

            if($type === 'dribbble') {
                $dx = floor(($social_image_width - $meta['sizes'][$type.'-image']['width'])/2);
                $dy = floor(($social_image_height - $meta['sizes'][$type.'-image']['height'])/2);
            } else {
                $dx = 0;
                $dy = 0;
            }

            imagecopymerge_alpha($base, $img, $dx, $dy, 0, 0, $meta['sizes'][$type.'-image']['width'],$meta['sizes'][$type.'-image']['height'],100);
            imagedestroy($img);

            imagepng($base, $file_path, 9);
            imagedestroy($base);

        }

    }

}

?>

Now, 95% of the above process was super simple. Aside from two small things.

The first hurdle I hit was that the standard imagecopymerge function doesn’t work with images containing alpha transparency (which, to be honest, is a bit weird), I found half a dozen different implementations of an imagecopymerge_alpha function online, they all seemed to work, but the one I went with was the simplest.

<?php

function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) {

    // creating a cut resource
    $cut = imagecreatetruecolor($src_w, $src_h);

    // copying relevant section from background to the cut resource
    imagecopy($cut, $dst_im, 0, 0, $dst_x, $dst_y, $src_w, $src_h);
    // copying relevant section from watermark to the cut resource
    imagecopy($cut, $src_im, 0, 0, $src_x, $src_y, $src_w, $src_h);
    // insert cut resource to destination image
    imagecopymerge($dst_im, $cut, $dst_x, $dst_y, 0, 0, $src_w, $src_h, $pct);
    
    imagedestroy($cut);

}

?>

The second issue only occurred when the image being added (in this case the transparent PNG uploaded into WordPress) was smaller than the image it was being merged with.

With each of the imagecopymerge_alpha function that I found the same issue was present, I’d get a big, black, unwanted bar in the final image.

After a little research I found that the reason was simple enough, the image resource that gets created by imagecreattruecolor has a black background. A black background I didn’t want.

I googled, and then I googled some more. I found “solutions” that had works for some, but they didn’t work for me. All I wanted was for that initial image resource to be created with a transparent background.

I got close at one point where I was able to set black as the image’s transparent colour. However this only worked if the image I’d uploaded didn’t contain any black. Not really ideal.

So I started breaking down the both my add_social_variant function and the imagecopymerge_alpha function bit by bit to find where I might be able to jump in a prevent the black bar from appearing.

In the end the issue was that I was passing the width and height of the template image into the final imagecopymerge_alpha call instead of the width and height of the image being merged on top (the transparent PNG variant generated by WordPress).


Step 5: Making use of the newly created social images

The Facebook and Twitter variants that I’ve created are specifically for use with Open Graph and Twitter Cards.

So, in the <head> of my single posts, I first grab the id of the post thumbnail/feature image (ie. the transparent PNG that I’ve uploaded), then use that to build the URL for the relevant social image created earlier, and output that with the rest of the Open Graph and Twitter Card meta tags.

<?php $post_thumbnail_id = get_post_thumbnail_id($post->ID); ?>
<meta property="og:image" content="<?php echo content_url('/uploads/social-image/facebook-'.$post_thumbnail_id.'.png'); ?>" />
<meta name="twitter:image" content="<?php echo content_url('/uploads/social-image/twitter-'.$post_thumbnail_id.'.png'); ?>" />

Step 6: More?

I’m not currently making automatic use of the Instagram and Dribbble variants, was hoping to auto-post to Instagram but their API doesn’t currently allow such a thing, and while I could auto-post to Dribbble, I don’t want to share every uploaded image there. I may also go back and refine this process further if the need arises, or if I find more efficient ways of doing things.

I’ll still be posting the images to an Instagram account, but it will have to be handled manually for the time being (also, why have Instagram added account-switching yet?).

At least I don’t have to manually create half a dozen different versions/layouts for every image I uploaded.

I'll post a couple of real-world examples once the site I'm using this on is live.

One image. One upload. Multiple options.

Magic.