I explored the patch "Keep fixed thumnail size" [sic] and appreciated its intent. (http://drupal.org/node/142654) However, I was unhappy with it -- the code was uncommented, there were spelling errors in the admin option (I've always considered it a bad sign when coders misspell), plus the new option was confusingly described, and on top of all that it just plain didn't work when I applied the patch. This inspired me to create a full-featured thumbnail-generation routine which has many more options and which, so far, seems to work perfectly (please comment if not!).

It works best if you also implement the "Re-resizing node images" patch at http://drupal.org/node/130069, which works wonderfully and I beg the maintainers to immediately add to the module. Implementing that patch allows you to resize all existing thumbnails after changing the settings I've added here--works wonderfully. (You can install my suggested patch, install the "Re-resizing node images" patch, and then update all your thumbnails in one swoop.)

Here's what my suggested code accomplishes:

New Admin Options

In Site Configuration > Node Images, you're given a new setting, "Thumbnail Generation Method," with 5 options to choose from:

  • Fit to width and Fit to height scale the thumbnail proportionally until it is the width or height specified in "Resolution for thumbnails" setting, maintaining the aspect ratio of the original and disregarding the other thumbnail dimension. Useful if all thumbnails must be a certain width but height can vary, or all must be a certain height but width can vary.
  • Fit to longest side is the TYPICAL, DEFAULT BEHAVIOR -- it scales the whole thing down so that whichever side is longer fits into the thumbnail "box." Original aspect ratio is maintained.
  • Fit to shortest side is similar but matches whichever side is SHORTER to its corresponding thumbnail dimension. With this, there can be some spillover so the whole thing might not fit in the thumbnail "box" dimensions. Aspect ratio maintained.
  • Crop and fit exactly is the gem; it fits the thumbnail to be EXACTLY the size specified above, cropping the longer side (centering the image in the crop rectangle).

Now when you choose one of these 5 options ("Fit to longest side" being the default, the same as the one already being used by the module), thumbnails will be generated using it. This way you can have all square thumbnails, or all fitting an exact rectangle size, or with the same width but varying heights, etc.

New Thumbnail Generation Routine

The thumbnail generation routine has been expanded to read and take advantage of the new configuration option -- but in a simple, understandable way. It uses my "Universal thumbnail calculator" function, which (along with the admin settings I created to take advantage of it) I'm hoping folks might want to take and implement in other modules.

The Code

This is unfortunately not a formal patch, because I've modified the node_images.module file in other ways and the patch might not be accurate. I'm really sorry about that. Maybe a maintainer can generate a patch and attach it in a followup.

So to add the new admin setting, find the following code in node_images.module:

  $form['node_images_thumb_resolution'] = array(
    '#type' => 'textfield',
    '#title' => t('Resolution for thumbnails'),
    '#default_value' => variable_get('node_images_thumb_resolution', '100x100'),
    '#size' => 15,
    '#maxlength' => 7,
    '#description' => t('The thumbnail size expressed as WIDTHxHEIGHT (e.g. 100x75).'),
  );

Then ADD the following immediately afterward:

  $form['node_images_thumb_sizemethod'] = array(
    '#type' => 'select',
    '#title' => t('Thumbnail Generation Method'),
    '#default_value' => variable_get('node_images_thumb_sizemethod', 'fitLongestSide'),
    '#description' => t('This determines the way node_images calculates the size and aspect ratio of thumbnails. <strong>Fit to width</strong> or <strong>Fit to height</strong> scale the thumbnail proportionally until it is the width or height specified above, maintaining the aspect ratio of the original and disregarding the other thumbnail dimension. Useful if all thumbnails must be a certain width but height can vary, or all must be a certain height but width can vary. <strong>Fit to longest side</strong> is the TYPICAL, DEFAULT BEHAVIOR -- it scales the whole thing down so that whichever side is longer fits into the thumbnail "box." Original aspect ratio is maintained. <strong>Fit to shortest side</strong> is similar but matches whichever side is SHORTER to its corresponding thumbnail dimension. With this, there can be some spillover so the whole thing might not fit in the thumbnail "box" dimensions. Aspect ratio maintained. <strong>Crop and fit exactly</strong> is the gem; it fits the thumbnail to be EXACTLY the size specified above, cropping the longer side (centering the image in the crop rectangle).'),
    '#options' => array(
      'fitToWidth' => t('Fit to width'),
      'fitToHeight' => t('Fit to height'),
      'fitLongestSide' => t('Fit to longest side'),
      'fitShortestSide' => t('Fit to shortest side'),
      'fitExactSize' => t('Crop and fit exactly')
    ),
  );

For the actual workhorse code, you simply REPLACE the entire "_node_images_create_thumbnail" function with two new ones. Find the following code:

/**
 * Create the thumbnail for the uploaded image.
 */
function _node_images_create_thumbnail($path, $suffix='_tn') {

and so on, until the final closing bracket of the function. Delete this entire function and replace with the following code (which I'm highlighting PHP so you can see how it works--remember not to paste the opening and closing PHP brackets):

/**
 * A universal thumbnail generator function. Provides several possible methods for calculating and/or cropping
 * the actual thumbnail, and returns an array of properties you can then use to invoke whatever image toolkit
 * you wish to do the actual image manipulation and disk work.
 * 
 * @param $originalWidth
 *    The width in pixels of the original image
 * @param $originalHeight
 *    The height in pixels of the original image
 * @param $suggestedWidth
 *    The width in pixels that you or your program's admin panel have specified for the thumbnail width
 * @param $suggestedHeight
 *    The height in pixels that you or your program's admin panel have specified for the thumbnail width
 * @param $resizeMethod
 *    A string. The magic value used to decide how to generate the thumbnail. It is suggested that in
 *    the code which calls this function, you run a test of your own design to determine which value to
 *    pass. For example, you could have an administration screen somewhere which allows a user to determine
 *    it, your code reads the configuration value, and passes the appropriate flag to this function.
 *    Must be one of the following five values:
 * 
 *    'fitToWidth':     Scales the thumbnail proportionally until it is the suggested width.
 *    'fitToHeight':    Scales the thumbnail proportionally until it is the suggested height.
 *    'fitLongestSide': Picks the LONGEST dimension of the image, then scales the thumbnail proportionally
 *                      to match suggested size. Result: the image will fit inside the suggested dimensions
 *                      every time, but often will not fill up the whole box except in the one direction.
 *    'fitShortestSide':Picks the SHORTEST dimension of the image, then scales the thumbnail proportionally
 *                      to match suggested size. Result: the longer dimension may "stick out" past its suggested size.
 *    'fitExactSize':   Performs fitShortest, then centers the image and crops off the extra so thumbnail fits
 *                      EXACTLY in the suggested box dimensions, every single time.
 * @return
 *    This function returns an array containing:
 *      'scaledX':  The newly-calculated width of the thumbnail
 *      'scaledY':  The newly-calculated height of the thumbnail
 *      'offsetX':  If cropping is required, this will be the amount from the left to offset the image
 *      'offsetY':  If cropping is required, this will be the amount from the top to offset the image
 *      'croppedX': The width of the thumbnail after cropping
 *      'croppedY': The height of the thumbnail after cropping
 *  
 */
function universal_thumbnail_calculator( $originalWidth, $originalHeight, $suggestedWidth, $suggestedHeight, $resizeMethod = 'fitLongestSide' ) {
  
  $ratioX       = $suggestedWidth/$originalWidth;
  $ratioY       = $suggestedHeight/$originalHeight;
  $crop = FALSE;
  
  if ($resizeMethod == 'fitExactSize') {
    $resizeMethod = 'fitShortestSide';
    $crop = TRUE;
  }
  if ($resizeMethod == 'fitLongestSide') {
    $resizeMethod = ($originalWidth > $originalHeight) ? 'fitToWidth' : 'fitToHeight';
  }
  if ($resizeMethod == 'fitShortestSide') {
    $resizeMethod = ($originalWidth < $originalHeight) ? 'fitToWidth' : 'fitToHeight';
  }
  
  switch ($resizeMethod) {
    case 'fitToWidth':
      $thumbnailValues = array( 'scaledX' => $suggestedWidth, 'scaledY' => round($originalHeight*$ratioX) );
    break;
    case 'fitToHeight':
      $thumbnailValues = array( 'scaledX' => round($originalWidth*$ratioY), 'scaledY' => $suggestedHeight );
    break;
  }
  
  if ($crop) {
		// Calculate the offset (where to begin 'drawing' the crop rectangle) as half the pixel overflow:
		$thumbnailValues['offsetX'] = round($thumbnailValues['scaledX'] - $suggestedWidth)/2;
		$thumbnailValues['offsetY'] = round($thumbnailValues['scaledY'] - $suggestedHeight)/2;
		// Pass along the "newly cropped" dimensions to be used by later code:
		$thumbnailValues['croppedX'] = $suggestedWidth;
		$thumbnailValues['croppedY'] = $suggestedHeight;
  }

  return $thumbnailValues;
}

/**
 * Create the thumbnail for the uploaded image.
 */

function _node_images_create_thumbnail($path, $suffix='_tn') {
  $size = variable_get('node_images_thumb_resolution', '100x100');
  list($width, $height) = explode('x', $size);
  $dest_path = preg_replace('!(\.[^/.]+?)?$!', "$suffix\\1", $path, 1);
  $resizeThumbMethod = variable_get('node_images_thumb_sizemethod', 'fitLongestSide');
  
  if ($size = getimagesize($path)) {
		$currentWidth = $size[0];
		$currentHeight = $size[1];      
		$thumbnailDimensions = universal_thumbnail_calculator( $currentWidth, $currentHeight, $width, $height, $resizeThumbMethod);
		
		image_resize($path, $dest_path, $thumbnailDimensions['scaledX'], $thumbnailDimensions['scaledY']);
		if ((isset($thumbnailDimensions['offsetX']) && ($thumbnailDimensions['offsetX']>0)) || (isset($thumbnailDimensions['offsetY']) && ($thumbnailDimensions['offsetY']>0))) {
			image_crop($dest_path, $dest_path, $thumbnailDimensions['offsetX'], $thumbnailDimensions['offsetY'], $thumbnailDimensions['croppedX'], $thumbnailDimensions['croppedY']);
		}
		
		$info = image_get_info($dest_path);
		$thumb = new stdClass();
		$thumb->filename = basename($dest_path);
		$thumb->filepath = $dest_path;
		$thumb->filesize = $info['file_size'];
		$thumb->filemime = $info['mime_type'];
		return $thumb;
  }
  return NULL;
}

I hope this helps some people. I think it's worthy of inclusion in the core module and hope the maintainers will commit it in a future release.

Comments

oriol_e9g’s picture

I test it and works fine for me.

I would like to add that this feature fix this bug: http://drupal.org/node/120018

Vallenwood’s picture

Hey, cool. Unintended side benefit.

Conflict Warning: Something I recently noticed: If by any chance you have implemented the "Patch: resize uploaded image and other enhancements" patch at http://drupal.org/node/145174, this actually causes a conflict that can mess up the thumbnail generation. That particular patch renames the entire _node_images_create_thumbnail function and replaces it with its own _node_images_resize variation. If you want to use some of the enhancements of this other patch (like specifying a maximum size for uploaded images), you'll have to do what I did and 1) make sure you haven't deleted my patched _node_images_create_thumbnail function, and 2) find all instances where _node_images_resize is called to create a thumbnail, and replace it with the original _node_images_create_thumbnail call. If you do NOT do this, or do it incorrectly, you could get weird behavior such as thumbnail generating wrong the first time, or not generating at all.

So to summarize: as of May 31 2007, the "Patch: resize uploaded image and other enhancements" patch (node_images-070519.patch) conflicts with this patch and you'll have to manually hack that other patch to restore functionality. It is possible to use them together (I do) but requires some hacking. (I consider that other patch to be badly-behaved code, as it renames a core module function, destroying any other patches or future module versions that may utilize it.)