Rotating original file for image field in Drupal 7 and dealing with browser cache

While working on new Qwintry.com tasks we needed to provide our operators the interface to rotate uploaded images (and I wanted to rotate the original image file). Surprisingly, I could not find anything like that among d.org modules so I have to come up with my own solution. I was expecting to finish this task by 1 hour, but, as it often happens, the way to right solution took a bit longer.

For the final code scroll down to the end of the post, since now I will be showing some ugly code that you don't really need :)

Here is the UI for operators:

These links are created in the node--[type].tpl.php in my theme:

<a href="<?= url('admin/rotate/' . $node->field_photo[LANGUAGE_NONE][0]['fid'] . '/cw/field_photo') ?>">&#8635; Rotate CW</a>  
<a href="<?= url('admin/rotate/' . $node->field_photo[LANGUAGE_NONE][0]['fid'] . '/ccw/field_photo') ?>">&#8634; Rotate CCW</a>

the first version of the function:

<?php  
function bdr_warehouse_rotate_image($fid, $direction, $field_name) {  
  $file = file_load($fid);

  $img = image_load($file->uri);
  image_rotate($img, $direction == 'cw' ? 90 : -90);
  image_path_flush($file->uri);
  $result = image_save($img);
  if ($result) {
    $nid = db_query("SELECT entity_id FROM {field_data_{$field_name}} WHERE {$field_name}_fid=:fid", array(':fid' => $fid))->fetchField();


    db_query("UPDATE {file_managed} SET filesize=:size WHERE fid=:fid", array(':size' => $img->info['file_size'], ':fid' => $fid));
    db_query("UPDATE {field_data_{$field_name}} SET {$field_name}_width=:width, {$field_name}_height=:height WHERE {$field_name}_fid=:fid LIMIT 1", 
      array(':width' => $img->info['width'], ':height' => $img->info['height'], ':fid' => $fid));

    db_query("UPDATE {field_revision_{$field_name}} SET {$field_name}_width=:width, {$field_name}_height=:height WHERE {$field_name}_fid=:fid LIMIT 1", 
      array(':width' => $img->info['width'], ':height' => $img->info['height'], ':fid' => $fid));


    cache_clear_all("field:node:$nid", 'cache_field');
    drupal_set_message('Image rotated! Use ctrl+f5 if the image preview is not rotated - it is your browser cache.');
  }

  if (!empty($_SERVER['HTTP_REFERER'])) {
    $mark = strpos($_SERVER['HTTP_REFERER'], '?') === false ? '?' : '&';
    drupal_goto($_SERVER['HTTP_REFERER'] . $mark . 'refresh=1');
  }

}
?>

Take note that we flush image presets cache, node field cache, and also update the sizes of the picture in three places - in filemanaged table, in fielddata{fieldname} table, and in fieldrevision{field_name} table.

But it turned out that it is not enough so I had to come up with some way to force chrome to reload picture, so we pass ?refresh=1 to the url and we had this in our init hook:

<?php  
function bdr_warehouse_init() {  
  if (!empty($_REQUEST['refresh'])) {
    drupal_add_js(drupal_get_path('module', 'bdr_warehouse') . '/scripts/img_refresh.js');
  }

}
?>  

and the contents of the javascript file were like:

function updateQueryStringParameter(uri, key, value) {  
  var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
  var separator = uri.indexOf('?') !== -1 ? "&" : "?";
  if (uri.match(re)) {
    return uri.replace(re, '$1' + key + "=" + value + '$2');
  }
  else {
    return uri + separator + key + "=" + value;
  }
}

setTimeout(function() {  
    // force image refresh in browsers after rotation
  var x = document.querySelectorAll(".field-type-image a, .image-widget-data a");

  var i;
  for (i = 0; i < x.length; i++) {
    x[i].href = updateQueryStringParameter(x[i].href, 'rand', new Date().getTime());
  }  


  var x = document.querySelectorAll(".field-type-image img, .image-widget img");
  var i;
  for (i = 0; i < x.length; i++) {
    x[i].src = updateQueryStringParameter(x[i].src, 'rand', new Date().getTime());
  }

}, 300);

I had to enclose the image refresh function to setTimeout because Drupal was throwing 503 errors if the image was reloaded too quickly (it is the image.module locking mechanism at work). BTW, don't use this updateQueryStringParameter since awesome jquery.bbq is included in drupal 7 core :) (but you dont need this js piece of code at all in the final version so read further)

So, we used this first version for a day on a production and it turned out that the cache refresh in chrome still did not work properly for image presets - sometimes it kept showing old, not rotated version of the picture, skewed, like this:

I also realized that I want to show the rotate buttons in the node edit, in the image file widget, as well.

Here comes the second version of the function which renames the image file by adding some symbols at the end of the file (and it forces chrome to reload the original file and all the image presets, a lot more reliably than the javascript refreshing mechanism above). Finally, it works! And we can get rid of all javascript mentioned above.

Here is the

Final code:


the main function:

<?php  
   function bdr_rotate_image($fid, $direction, $field_name) {
  $file = file_load($fid);

  $img = image_load($file->uri);
  image_rotate($img, $direction == 'cw' ? 90 : -90);
  $result = image_save($img);
  if ($result) {
    $uri = $file->uri;
    $ext = substr($uri, -3); // Change this if you expect some weird extensions like .jpeg !
    $new_uri = substr($uri, 0, -4) . '_1' . '.' . $ext;
    file_move($file, $new_uri);
   // it is not completely ok to pass php variables into sql just like that, but we do it here since the input is always safe in my case
    $nid = db_query("SELECT entity_id FROM {field_data_{$field_name}} WHERE {$field_name}_fid=:fid", array(':fid' => $fid))->fetchField();


    db_query("UPDATE {file_managed} SET filesize=:size WHERE fid=:fid", array(':size' => $img->info['file_size'], ':fid' => $fid));
    db_query("UPDATE {field_data_{$field_name}} SET {$field_name}_width=:width, {$field_name}_height=:height WHERE {$field_name}_fid=:fid LIMIT 1", 
      array(':width' => $img->info['width'], ':height' => $img->info['height'], ':fid' => $fid));
    cache_clear_all("field:node:$nid", 'cache_field');
    drupal_set_message('Image rotated!');
  }

  if (!empty($_SERVER['HTTP_REFERER'])) {
    drupal_goto($_SERVER['HTTP_REFERER']);
  }

}
?>

the node edit field widget override with rotation links (put in your theme template.php):

<?php

function YOURTHEMENAME_image_widget($variables) {  
  $element = $variables['element'];
  $output = '';
  $output .= '<div class="image-widget form-managed-file clearfix">';

  if (isset($element['preview'])) {
    $output .= '<div class="image-preview">';
    $output .= drupal_render($element['preview']);
    if (user_access('create page content')) {
        $output .= "<br> <a href='" . url('admin/rotate/' . $element['#file']->fid . '/cw/' . $element['#field_name']) . "'>&#8635; Rotate CW</a>";
        $output .= "&nbsp;&nbsp; <a href='" . url('admin/rotate/' . $element['#file']->fid . '/ccw/' . $element['#field_name']) . "'>Rotate CCW &#8634;</a>";
    }
    $output .= '</div>';
  }

  $output .= '<div class="image-widget-data">';
  if ($element['fid']['#value'] != 0) {
    $element['filename']['#markup'] .= ' <span class="file-size">(' . format_size($element['#file']->filesize) . ')</span> ';
  }
  $output .= drupal_render_children($element);
  $output .= '</div>';
  $output .= '</div>';

  return $output;
}

?>

The registered path in YOURMODULE_menu hook:

<?php

$items['admin/rotate'] = array(
    'title' => 'Rotate',
    'page callback' => 'bdr_rotate_image',
    'page arguments' => array(2, 3, 4),
    'access arguments' => array('create page content'),
  );
?>

Anton Sidashin

I am a web developer and CTO with 15+ years of experience, and I am passionate about performance, usability, and getting things done.

Samara, Russia