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 file_managed table, in field_data_{field_name} table, and in field_revision_{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'),
  );
?>

Comments

Thank you for the tuts.
I tried to follow but the problem I do not understand the links :/cw/..,/ccw/..
admin/rotate/ how did you worked this .
So interesting i m looking for something like what you ve done .

26 September, 2015

Thank you for your code,

I am still having an issue, the links show, but when i click them it redirects me to an unavailable page:

WEBSITE/admin/rotate/988/ccw/field_image_blog

and nothing happens. I would greatly appreciate your help.

14 October, 2015

https://www.drupal.org/node/2215369

Add this patch - and u will be saved...No need to add the above code.

09 March, 2016

This code worked wonderfully! Thank you for sharing it. I made one minor change to the way the extension is getting pulled. I changed:

$ext = substr($uri, -3);

to

$ext = pathinfo($uri, PATHINFO_EXTENSION);

19 May, 2016

Thx a lot i was stock for 2 days ! :D

Seb

31 May, 2016

Post new comment

Private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options

Note for potential spammers: all links in your comment will not be indexed by search engines.

Anton Sidashin

Anton Sidashin senior developer, Pixeljets co-founder

I'm a web developer specializing in PHP and Javascript, and Drupal, of course. I'm building Drupal projects since 2005, and I was working as full-time senior engineer in CS-Cart for a while, building revolutionary e-commerce software. In my free time, I enjoy playing soccer, building my body in gym, and playing guitar.

Drupal.org ID: restyler
Drupal association member