Index: image-5--2-dev/image.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/image/image.module,v
retrieving revision 1.258.2.7
diff -u -u -p -r1.258.2.7 image.module
--- image-5--2-dev/image.module	18 Apr 2008 00:32:10 -0000	1.258.2.7
+++ image-5--2-dev/image.module	8 May 2008 15:25:54 -0000
@@ -90,11 +90,72 @@ function image_admin_settings() {
     '#type' => 'fieldset',
     '#title' => t('File paths')
   );
-  $form['paths']['image_default_path'] = array(
+
+  $example_tokens = array(
+    '%nodepath'     => 'safe/url/example' ,
+    '%filename'     => 'sample-filename',
+    '%filename_raw' => 'Sample filename',
+    '%label'        => 'thumb',
+    '%extension'    => 'jpg',
+    '%extension_raw'=> 'JPEG',
+    '%nid'          => 666,
+    '%uid'          => $GLOBALS['user']->uid,
+    '%date'         => date('Y-m-d'),
+    '%year'         => date('Y'),
+    '%month'        => date('m'),
+    '%day'          => date('d'),
+  );
+  $available_vocabs = taxonomy_get_vocabularies('image');
+  $vocablist = '';
+  foreach($available_vocabs as $voc) {
+    $example_tokens['%vocab-'. $voc->vid] = 'a-term';
+    $vocablist .= ' %vocab-'. $voc->vid .' = '. $voc->name .' ';
+   }
+
+  $example_path = variable_get('file_directory_path', 'files') .'/'. strtr(variable_get('image_derivatives_filename_pattern', _image_default_filename_pattern()), $example_tokens );
+
+  $form['paths']['image_derivatives_filename_pattern'] = array(
     '#type' => 'textfield',
-    '#title' => t('Default image path'),
-    '#default_value' => variable_get('image_default_path', 'images'),
-    '#description' => t('Subdirectory in the directory "%dir" where pictures will be stored. Do not include trailing slash.', array('%dir' => variable_get('file_directory_path', 'files'))),
+    '#title' => t('Filename naming pattern'),
+    '#default_value' => variable_get('image_derivatives_filename_pattern', _image_default_filename_pattern()),
+    '#description' => t('
+      Pattern to use when saving or creating derivatives (like thumbnails) for an image.
+      Available tokens are: %tokens.<br/><pre>%example_path</pre>
+      ',
+      array(
+        '%tokens' => join(', ', array_keys($example_tokens)),
+        '%example_path' => $example_path,
+        '%vocablist' => $vocablist,
+      )
+    ),
+  );
+  $form['paths']['image_derivatives_filename_help'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Filename pattern help'),
+    '#collapsible' => TRUE, '#collapsed' => TRUE,
+    '#description' => t('
+      <b>%filename</b> is a sanitized version of the upload name, with special characters converted to "-" and lowercased.
+      If you want to keep the messy filename, use <b>%filename_raw</b>.
+      <b>%extension</b> (eg "jpg") will be forced to lowercase unless you use <b>%extension_raw</b>.<br />
+      <b>%vocab-n</b> will be replaced with the a term from the named vocabulary: %vocablist. <br />
+      Missing elements will be collapsed cleanly if possible.
+      (<b>images/%vocab-2/%filename.%label.%extension</b> will become <b>images/%filename.%extension</b> if no vocab-2 term or label is present).
+      This pattern is re-run any time "rebuild derivatives" happens to an image, so it may cause sweeping changes and file links getting lost!
+      <br/>However, to make this change take effect on existing images, you may have to visit !admin-content and choose to apply the changes to the files yourself.
+      ',
+      array(
+        '%vocablist' => $vocablist,
+        '!admin-content' => l('admin/content', 'admin/content/node')
+      )
+    ),
+  );
+
+  // rename/move originals
+  $form['image_move_originals'] = array(
+    '#type' => 'checkbox',
+    '#default_value' => variable_get('image_move_originals', FALSE),
+    '#title' => t('Force renaming of original'),
+    '#description' => t('Normally previously-uploaded images remain where they were put, but if you are using the filename pattern to organise your directories, we want to move them also.Set this option, and the next time images are rebuilt, the original file will be renamed also.'),
   );
 
   $form['image_max_upload_size'] = array(
@@ -174,6 +235,15 @@ function image_admin_settings() {
 }
 
 /**
+ * Return the default filename pattern, incorporating the old 'images' directory
+ * path if appropriate.
+ */
+function _image_default_filename_pattern() {
+  // image_default_path is no longer used, but referenced here for backwards compatability
+  return variable_get('image_default_path', 'images') .'/%filename_raw.%label.%extension_raw';
+}
+
+/**
  * Check that the sizes provided have the required amount of information.
  */
 function image_settings_sizes_validate(&$form) {
@@ -322,6 +392,7 @@ function image_operations_rebuild($nids)
     if ($node = node_load($nid)) {
       if ($node->type == 'image') {
         $node->rebuild_images = TRUE;
+        drupal_set_message(t("Rebuilding %node-title's resized images.", array('%node-title' => $node->title)));
         image_update($node);
       }
     }
@@ -357,7 +428,7 @@ function image_prepare(&$node, $field_na
     }
 
     // Save the file to the temp directory.
-    $file = file_save_upload($field_name, _image_filename($file->filename, IMAGE_ORIGINAL, TRUE));
+    $file = file_save_upload($field_name, _image_filename($file->filename, IMAGE_ORIGINAL, TRUE, $node));
     if (!$file) {
       return;
     }
@@ -733,6 +804,14 @@ function image_update(&$node) {
       // Find the original image.
       $original_file = db_fetch_object(db_query("SELECT i.fid, f.filepath FROM {image} i INNER JOIN {files} f ON i.fid = f.fid WHERE i.nid = %d AND image_size = '%s'", $node->nid, IMAGE_ORIGINAL));
 
+			if(! file_exists($original_file->filepath)) {
+	      if (image_access('update', $node)) {
+	      	// Only show the message if they have a chance to understand it
+				  drupal_set_message(t("Unable to locate original file %filepath - Something's gone missing. Not rebuilding sizes as that may cause data loss. Not updating image node [!node] . You may possibly fix this be replacing the image. ", array('%filepath' => $original_file->filename, '!node' => l($node->title, 'node/'. $node->nid .'/edit'))), 'error');
+	      }
+				return;
+			}
+
       // Delete all but the original image.
       $result = db_query("SELECT i.fid, f.filepath FROM {image} i INNER JOIN {files} f ON i.fid = f.fid WHERE i.nid = %d AND f.fid <> %d", $node->nid, $original_file->fid);
       while ($file = db_fetch_object($result)) {
@@ -744,6 +823,23 @@ function image_update(&$node) {
         db_query("DELETE FROM {image} WHERE fid = %d", $file->fid);
       }
 
+      // rename/move originals to match updated filename patterns ////
+      if (variable_get('image_move_originals', FALSE)) {
+        $new_filename = _image_filename($original_file->filepath, IMAGE_ORIGINAL, FALSE, $node);
+        if (($original_file->filepath != $new_filename) && file_exists($original_file->filepath)) {
+          drupal_set_message("Moving original image from '$original_file->filepath' to '$new_filename' ");
+          if (file_move($original_file->filepath, $new_filename)) {
+						// image_insert is not expecting the original to be changable.
+		        db_query("DELETE FROM {files} WHERE nid = %d AND filename = '%s'", $node->nid, IMAGE_ORIGINAL);
+    		    db_query("DELETE FROM {image} WHERE nid = %d AND image_size = '%s'", $node->nid, IMAGE_ORIGINAL);
+            _image_insert($node, IMAGE_ORIGINAL, $new_filename);
+          }
+          else {
+            drupal_set_message("Failed to move original image from ". $original_file->filepath ." to $new_filename ", 'error');
+          }
+        }
+      }
+
       _image_build_derivatives($node, FALSE);
 
       // Display a message to the user if they're be able to modify the node
@@ -956,7 +1052,7 @@ function _image_build_derivatives(&$node
   // Resize for the necessary sizes.
   $image_info = image_get_info($original_path);
   foreach ($needed_sizes as $key => $size) {
-    $destination = _image_filename($original_path, $key, $temp);
+    $destination = _image_filename($original_path, $key, $temp, $node);
 
     $status = FALSE;
     switch ($size['operation']) {
@@ -994,30 +1090,143 @@ function _image_build_derivatives(&$node
 
 /**
  * Creates an image filename.
+ *
+ * Uses a token template string to invent new filename and folder structure for each derivative file.
  */
-function _image_filename($filename, $label = IMAGE_ORIGINAL, $temp = FALSE) {
-  $path = variable_get('image_default_path', 'images') .'/';
+function _image_filename($filename, $label = IMAGE_ORIGINAL, $temp = FALSE, $node = NULL) {
+  $filename = basename($filename);
+  $pos = strrpos($filename, '.');
+
+  // Build filename according to token pattern.
+  // When previewing a new (temp) node, we don't know the nid, but it should be available by submit time, so it'll work out.
+  $tokens = array(
+    '%nodepath'  => $node->path ? $node->path : $node->nid ? 'node-'. $node->nid : '',
+    '%filename'  => image_cleanstring(substr($filename, 0, $pos)),
+    '%filename_raw'  => substr($filename, 0, strrpos($filename, '.')),
+    '%label'     => $label,
+    '%extension' => strtolower(substr($filename, $pos+1)),
+    '%extension_raw' => substr($filename, $pos+1),
+    '%nid'       => $node->nid,
+    '%uid'       => $node->uid,
+    '%date'      => date('Y-m-d'),
+    '%year'      => date('Y'),
+    '%month'     => date('m'),
+    '%day'       => date('d'),
+  );
+
+  // Initialize null vocab placeholders to avoid tokens coming through
+  $available_vocabs = taxonomy_get_vocabularies('image');
+  foreach ($available_vocabs as $voc) { $tokens['%vocab-'. $voc->vid] = ''; }
+
+  // When called from image_update or elsewhere, the full node_load may not have happened, so terms are no yet available
+  if (empty($node->taxonomy)) {
+    // Fetch them.
+   $node->taxonomy = taxonomy_node_get_terms($node->nid);
+  }
+  // In other cases the 'taxonomy' array may be all sorts of different structures. Needs deep parsing.
+ 	// This term parser may be called repeatedly per image, and in bulks, so it's sorta cached.
+  static $term_tokens;
+  if (! $node->nid || ! $term_tokens[$node->nid]) {
+    $term_tokens[$node->nid] = image_node_term_tokens($node->taxonomy);
+  }
+	$tokens = array_merge($tokens, $term_tokens[$node->nid]);
+
+  // Even original images may be renamed, but they won't include the derivative label
+  if ($label == IMAGE_ORIGINAL) {
+    $tokens['%label'] = '';
+  }
+
+  $pattern = variable_get('image_derivatives_filename_pattern', _image_default_filename_pattern());
+  $filepath = strtr($pattern, $tokens);
+
+  // Sanitize possible typos and collapse dividers around tokens that are not there. More than one slash,underscore,dash or dot turn into just one whatever
+  // TODO test edge cases
+  $filepath = preg_replace('|([/_\-\.])[/_\-\.]+|', '$1', $filepath);
+
   if ($temp) {
-    $path .= 'temp/';
+    // TODO Is this right? Where should temp files go now?
+    $filepath = 'temp/'. $filepath;
+    }
+
+  $fullpath = file_directory_path() .'/'. $filepath;
+
+  // When using advanced filename patterns, we may need to ensure a directory to put this image into exists
+  $target_dir = dirname($fullpath);
+  if (! is_dir($target_dir)) {
+    image_mkdirs($target_dir);
   }
+  # dpm(array('parsed that the node '=>$node, 'gets a filename '=> $fullpath));
+  return $fullpath;
+}
 
-  $filename = basename($filename);
+/**
+ * Remove dodgy characters from filenames and parts.
+ * Force to lower and seperate with "-"
+ */
+function image_cleanstring($string) {
+  // Trim any leading or trailing separators
+  $string = preg_replace('/[^a-z0-9_\.]+/', '-', strtolower(trim($string)));
+  $string = preg_replace("/^\-+|\-+$/", "", $string);
+  return $string;
+}
+
+/**
+ * The taxonomy array may be of (at least) four different formats depending on how we are being called and the vocab rules!
+ * Need to parse it all out to deduce the term labels to use as tokens.
+ * 
+ * Calling this repeatedly is probably inefficient. But I don't want to mess with the expected taxonomy layout and mess things up.
+ * 
+ * @param 'taxonomy' array, as from $node->taxonomy.
+ * @return array of tokens, keyed by vocab identifier
+ */
+function image_node_term_tokens($taxonomy) {
+  // TODO Weighting? Which to choose when there's multiples selected within a vocab?
+  // Currently just grabbing the first one we find.
+  
+  if (empty($taxonomy)) { return array(); }
+	$tokens = array();
 
-  // Insert the resized name in non-original images
-  if ($label && ($label != IMAGE_ORIGINAL)) {
-    $pos = strrpos($filename, '.');
-    if ($pos === false) {
-      // The file had no extension - which happens in really old image.module
-      // versions, so figure out the extension.
-      $image_info = image_get_info(file_create_path($path . $filename));
-      $filename = $filename .'.'. $label .'.'. $image_info['extension'];
+  // Sort terms into vocabulary bags and choose one
+  if ($tags = $taxonomy['tags']) {
+    // In freetagging submission, the taxonomy may still be plaintext. Parse it if I can.
+    // $node->taxonomy = array('tags' => "words, here");
+    foreach ($tags as $vid => $tagstring) {
+      $tag_array = explode(',', $tagstring);
+      $top_tag = array_shift($tag_array); 
+      $tokens['%vocab-'. $vid] = image_cleanstring($top_tag);
     }
-    else {
-      $filename = substr($filename, 0, $pos) .'.'. $label . substr($filename, $pos);
+  }
+  else {
+    foreach ($taxonomy as $termthing) {
+    	// Figure out what termthing really is.
+		  if (is_object($termthing)) {
+      	// $node->taxonomy = array( 17 => Object(tid => 17, vid => 2, name => 'word' ) );
+		  	// it's OK
+        if(! $tokens['%vocab-'. $termthing->vid]) {
+	        $tokens['%vocab-'. $termthing->vid] = image_cleanstring($termthing->name);
+        }
+		  }
+		  else if (is_numeric($termthing)) {
+      	// $node->taxonomy = array( 2 => 17 );
+		    $term = taxonomy_get_term($termthing);
+        if(! $tokens['%vocab-'. $term->vid]) {
+		      $tokens['%vocab-'. $term->vid] = image_cleanstring($term->name);
+        }
+		  }
+		  else if (is_array($termthing)) {
+		  	// Actually the tids of several terms nested inside a vocab container;
+      	// $node->taxonomy = array( 2 => array(17, 18) );
+      	foreach ($termthing as $tid) {
+			    $term = taxonomy_get_term($tid);
+	        if(! $tokens['%vocab-'. $term->vid]) {
+		        $tokens['%vocab-'. $term->vid] = image_cleanstring($term->name);
+	        }
+      	}
+		  }
     }
   }
 
-  return file_create_path($path . $filename);
+	return $tokens;
 }
 
 /**
@@ -1094,7 +1303,7 @@ function _image_is_required_size($size) 
  */
 function _image_insert(&$node, $size, $image_path) {
   $original_path = $node->images[IMAGE_ORIGINAL];
-  if (file_move($image_path, _image_filename($original_path, $size))) {
+  if (file_move($image_path, _image_filename($original_path, $size, FALSE, $node)) || ($image_path == $original_path)) {
     // Update the node to reflect the actual filename, it may have been changed
     // if a file of the same name already existed.
     $node->images[$size] = $image_path;
@@ -1181,3 +1390,11 @@ function image_create_node_from($filepat
   return $node;
 }
 
+/**
+ * Recursive mkdir. Utility func
+ */
+function image_mkdirs($strPath, $mode = 0777) {
+  if(! $strPath){ trigger_error("Null call to image_mkdirs()", E_USER_WARNING); }
+  return is_dir($strPath) or ( image_mkdirs(dirname($strPath), $mode) and mkdir($strPath, $mode) );
+}
+
