Adding Some CSS Classes to Images Programmatically

August 6, 2024

I recently ran into a situation where the easiest way to get some images to look the way they needed to was to use some PHP to add CSS classes based on the image characteristics. The important stuff here isn't really Drupal-specific, but I've left it in for context. This can be done either in a module or in a theme—the extra classes are added using template_preprocess_field__FIELD_ID():

/**
 * Implements template_preprocess_field__FIELD_ID().
 *
 * Add some additional classes to images.
 */
function MODULE_preprocess_field__FIELD_ID(array &$variables) {
  if (isset($variables['items']) && ($variables['items'])) {
    foreach ($variables['items'] as $delta => $item) {
      if (isset($item['content']['#item'])
        && ($file = $item['content']['#item']?->entity)
        && ($file instanceof File)
        && ($image_properties = _MODULE_image_properties($file))) {
        $variables['items'][$delta]['attributes'] ??= new Attribute();
        foreach ($image_properties as $image_property) {
          $variables['items'][$delta]['attributes']?->addClass("image-{$image_property}");
        }
      }
    }
  }
}

Note that this checks that the image file is a file entity, so at the top of the file:

use Drupal\file\Entity\File;

Finally, the helper function that examines each image in order to add the properties that are converted to classes by the _MODULE_image_properties() function:

/**
 * Determine an image's orientation and if it has an empty background.
 *
 * For the empty background test, the pixels adjacent to the four corner pixels
 * are checked (adjacent because at least one image in use has color everywhere
 * except in a 1-pixel-wide frame around the photo): if none has transparency
 * and at least two are white, the background is interpreted as empty. The
 * default threshold for "white" is slightly less than 255 * 3 (= 765) in case
 * there's some imperfection in the source image.
 */
function _wtccommunications_miscellaneous_image_properties(File $file, int $rgb_threshold = 760) {
  $properties = [];
  if (($file_mime_type = $file?->getMimeType())
    && ($uri = $file?->getFileUri())
    && ($stream_wrapper_manager = \Drupal::service('stream_wrapper_manager')?->getViaUri($uri))
    && ($file_realpath = $stream_wrapper_manager?->realpath())) {
    $test_image = FALSE;
    if ($file_mime_type == 'image/jpeg') {
      $test_image = imagecreatefromjpeg($file_realpath);
    }
    elseif ($file_mime_type == 'image/png') {
      $test_image = imagecreatefrompng($file_realpath);
    }
    if ($test_image) {
      $last_horizontal = imagesx($test_image) - 1;
      $last_vertical = imagesy($test_image) - 1;
      if ($last_horizontal < $last_vertical) {
        $properties[] = 'portrait';
      }
      elseif ($last_horizontal > $last_vertical) {
        $properties[] = 'landscape';
      }
      elseif ($last_horizontal == $last_vertical) {
        $properties[] = 'square';
      }
      $coordinates = [
        [0, 0],
        [$last_horizontal - 1, 0],
        [0, $last_vertical - 1],
        [$last_horizontal - 1, $last_vertical - 1],
      ];
      $empty_corner_count = 0;
      foreach ($coordinates as $coordinate_set) {
        if (($rgb = imagecolorat($test_image, $coordinate_set[0], $coordinate_set[1]))
          && ($colors = imagecolorsforindex($test_image, $rgb))) {
          if ($colors['alpha'] != 0) {
            $empty_corner_count = 0;
            break;
          }
          elseif (array_sum($colors) >= $rgb_threshold) {
            $empty_corner_count++;
          }
        }
      }
      if ($empty_corner_count >= 2) {
        $properties[] = 'background-empty';
      }
    }
  }
  return $properties;
}

A quick rundown of what's going on here (with a little more detail than appears in the function comment):

  1. The image is loaded from from the file entity passed in to the function and, if the image is a JPG or PNG, a new instance is created (using imagecreatefromjpeg() or imagecreatefrompng()); I don't think there's any reason imagecreatefromwebp() wouldn't also work here, but my project didn't require it and I didn't have full control over the hosting environment so didn't want to run the risk of a PHP error if WebP support were missing.
  2. The four corner coordinates are determined—one of them is, obviously, 0,0, the others are determined by subtracting 1 from the image width and height (because the width and height start at 1, but the coordinates start at 0).
  3. If the last horizontal coordinate is less than the last vertical, the image is portrait-oriented so “portrait” is added to the properties array; if the last horizontal coordinate is greater than the last vertical, “landscape” is added; if the two last coordinates are equal, “square” is added.
  4. If the image is not alpha-transparent, the pixel one pixel in from each corner is tested to see if it's white; if more than two of the pixels tested have colours adding up to the threshold or greater, “background-empty” is added to the properties array. A couple of additional caveats / explanations regarding this test:
    1. the corner pixel isn't used because I found that at least one image that doesn't have an empty background does have a single-pixel white border around it (causing it to incorrectly pass the “empty background” test); and
    2. actual “white” would require that the sum of the RGB values for a pixel be 765 (255 + 255 + 255), but 760 is used as the threshold (via a function parameter) in case there are JPG compression artifacts or other imperfections causing it to be not-quite-white.

That's the whole thing, and the end result is a couple of extra CSS classes that made it much easier to position and format each image to meet the project requirements.

Santa Monica Pier

Santa Monica Pier, July 2016