Creating a Drupal field formatter for responsive image output

Drupal hooks give a great deal of flexibility when it comes to formatting the output of a field, but largely this revolves around preprocessing fields or other similar hooks. This is good for a specific situation but does not give us flexibility for reuse in various situations.

We use Display Suite on a number of our sites in order to handle configuration of page output and our ideal solution for field output configuration would be to allow us to continue to configure display settings through Display Suite.

In this post we'll talk about the generic hooks to create a field formatter, as well as an example of how we used these hooks to solve our specific problem.

The specific problem: Drupal & responsive images

For this particular field formatter, rather than selecting a single image style for an image output, we wanted to set a mobile and a desktop image style for use on each image we were outputting. This should be configurable for each content type and view mode.

Having multiple images would allow us to output the two image URLs within the template. The secondary image could then be used as background element, data attribute (as part of a JavaScript image solution), srcset or picture attribute, among other solutions.

Defining the field formatter

When creating a new field formatter we must first specify the type of formatter we are creating, and the field it will be available to.

Here is our info hook in its entirety, before we dissect it below.

/**
 * Implements hook_field_formatter_info().
 */
function image_fallback_field_formatter_info() {
  $formatters = array(
    'image_fallback_formatter' => array(
      'label' => t('Image Fallback'),
      'field types' => array('image'),
      'settings' => array('image_style' => '', 'fallback_image_style' => '', 'image_link' => ''),
    ),
  );

  return $formatters;
}

Firstly the function name, image_fallback is our module name and it implements the _field_formatter_info() hook. This hook takes no parameters and returns an array of formatters (this means you can have more than one formatter to a module).

The formatters array is keyed by a unique formatter name image_fallback_formatter and takes the following parameters:

  • label - the name to display when selecting a field display type
  • field types - this is the field type(s) that can be used with this formatter; in our example we want this formatter to be available to just the image field.
  • settings - we will be revisiting this array of parameters, but for now it's enough to know that this array must contain an empty value for each of the parameters you are going to need to store (so best to leave this empty for now).

The field settings form

This is the point in the process where we need to decide precisely what options we plan to make available to the site administrator when setting up a field output.

It's important to remember at this point that we are not inheriting from an existing field formatter, we are creating a brand new formatter. Therefore any options that we want to use from the old formatter also need to be added into this settings form.

First up, here's our example use of hook_field_formatter_settings_form()

/**
 * Implements hook_field_formatter_settings_form().
 */
function image_fallback_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];

  // Retrieve the available image styles
  $image_styles = image_style_options(FALSE, PASS_THROUGH);

  // Add the original image style field
  // This is an existing setting of the image field
  $element['image_style'] = array(
    '#title' => t('Image style'),
    '#type' => 'select',
    '#default_value' => $settings['image_style'],
    '#empty_option' => t('None (original image)'),
    '#options' => $image_styles,
  );

  // Our first real addition, this field mirrors the settings of the image style above
  // This allows us to select a secondary "fallback" image style
  $element['fallback_image_style'] = array(
    '#title' => t('Fallback image style'),
    '#type' => 'select',
    '#default_value' => $settings['fallback_image_style'],
    '#empty_option' => t('None (original image)'),
    '#options' => $image_styles,
  );

  // List the available options for linking the image
  $link_types = array(
    'content' => t('Content'),
    'file' => t('File'),
  );

  // Add the image link field settings
  // This is an existing setting of the image field
  $element['image_link'] = array(
    '#title' => t('Link image to'),
    '#type' => 'select',
    '#default_value' => $settings['image_link'],
    '#empty_option' => t('Nothing'),
    '#options' => $link_types,
  );

  return $element;
}

If you are already familiar with Form API then most of this should make sense. It should be sufficient to say that we are defining the form elements used by Display Suite when editing the display of the field in the CMS.

We are adding three fields to the form: one to select the image style, a second new field to select an alternative image style, and the original image link field. The form fields shown here must be reflected in the settings array in the info field.

The field settings summary

If you are familiar with Display Suite, you will already know that the form is not shown for all fields by default (and I think we can all agree it would be a bit of a mess if it was). If you want to display field settings without the need to expand the settings list you can add a field summary to display the key settings.

/**
 * Implements hook_field_formatter_settings_summary().
 */
function image_fallback_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];

  $summary = array();

  // Retrieve the available image styles, as these contain the style names
  $image_styles = image_style_options(FALSE, PASS_THROUGH);
  // Unset possible 'No defined styles' option.
  unset($image_styles['']);
  // Styles could be lost because of enabled/disabled modules that defines
  // their styles in code.
  if (isset($image_styles[$settings['image_style']])) {
    $summary[] = t('Image style: @style', array('@style' => $image_styles[$settings['image_style']]));
  }
  else {
    $summary[] = t('Original image');
  }
  // Settings could also be lost if fallback image asset is disabled
  if (isset($image_styles[$settings['fallback_image_style']])) {
    $summary[] = t('Fallback image style: @style', array('@style' => $image_styles[$settings['image_style']]));
  }
  else {
    $summary[] = t('Fallback to original image');
  }

  $link_types = array(
    'content' => t('Linked to content'),
    'file' => t('Linked to file'),
  );
  // Display this setting only if image is linked.
  if (isset($link_types[$settings['image_link']])) {
    $summary[] = $link_types[$settings['image_link']];
  }

  return implode('<br />', $summary);
}

Outputting field contents

All that's left to do at this point is define how the field content is displayed to the end user, and this is done using hook_field_formatter_view(). Inside this hook we will loop through the items using this formatter (indexed by delta), and point them at the fallback image formatter theme.

/**
 * Implements hook_field_formatter_view().
 */
function image_fallback_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();

  // Check if the formatter involves a link type and if it does then create a link according to type
  if ($display['settings']['image_link'] == 'content') {
    $uri = entity_uri($entity_type, $entity);
  }
  elseif ($display['settings']['image_link'] == 'file') {
    $link_file = TRUE;
  }

  // For each of the items in this formatter view, 
  // we need to define the theme function that will control its display
  foreach ($items as $delta => $item) {
    if (isset($link_file)) {
      $uri = array(
        'path' => file_create_url($item['uri']),
        'options' => array(),
      );
    }
    // We need to setup the item (stored in $item) to be passed to the theme
    // Plus any configuration settings
    $element[$delta] = array(
      '#theme' => 'fallback_image_formatter',
      '#item' => $item,
      '#image_style' => $display['settings']['image_style'],
      '#fallback_image_style' => $display['settings']['fallback_image_style'],
      '#path' => isset($uri) ? $uri : '',
    );
  }

  return $element;
}

We then need to specify the theme function. I include this in the interest of completeness, but it follows all of the standard conventions of a theme function, and simply returns the output that we wish to display for the field.

At this point we have all of the settings we have configured available, and these can be used as needed.

/**
 * Returns HTML for a fallback image field formatter.
 *
 * @param $variables
 *   An associative array containing:
 *   - item: Associative array of image data, which may include "uri", "alt",
 *     "width", "height", "title" and "attributes".
 *   - image_style: An optional image style.
 *   - fallback_image_style: An optional image style.
 *   - path: An array containing the link 'path' and link 'options'.
 *
 * @ingroup themeable
 */
function theme_fallback_image_formatter($variables) {

  $item = $variables['item'];
  $image = array(
    'path' => $item['uri'],
  );

  if (array_key_exists('alt', $item)) {
    $image['alt'] = $item['alt'];
  }

  if (isset($item['attributes'])) {
    $image['attributes'] = $item['attributes'];
  }

  if (isset($item['width']) && isset($item['height'])) {
    $image['width'] = $item['width'];
    $image['height'] = $item['height'];
  }

  // Do not output an empty 'title' attribute.
  if (isset($item['title']) && drupal_strlen($item['title']) > 0) {
    $image['title'] = $item['title'];
  }

  if ($variables['image_style'] && $variables['fallback_image_style']) {
    $image['style_name'] = $variables['image_style'];
    $image['fallback_style_name'] = $variables['fallback_image_style'];
    $output = theme('fallback_image', $image);
  }
  else {
    $output = theme('image', $image);
  }

  // The link path and link options are both optional, but for the options to be
  // processed, the link path must at least be an empty string.
  if (isset($variables['path']['path'])) {
    $path = $variables['path']['path'];
    $options = isset($variables['path']['options']) ? $variables['path']['options'] : array();
    // When displaying an image inside a link, the html option must be TRUE.
    $options['html'] = TRUE;
    $output = l($output, $path, $options);
  }

  return $output;
}

The fallback_image or image theme functions are called based on fallback image style settings; the image theme function is already specified by the image module, while we add the fallback_image theme function to this module.

The function shown below is tailored to how we wish to output this additional data, but could be easily overridden with a template function. Ultimately it calls the image theme function with a fallback image data attribute added. This is generated from the specified fallback image style.

function theme_fallback_image($variables) {
  // Determine the dimensions of the styled image.
  $dimensions = array(
    'width' => $variables['width'],
    'height' => $variables['height'],
  );

  image_style_transform_dimensions($variables['style_name'], $dimensions);

  $variables['width'] = $dimensions['width'];
  $variables['height'] = $dimensions['height'];

  $original_image = $variables['path'];

  // Determine the URL for the styled image.
  $variables['path'] = image_style_url($variables['style_name'], $original_image);

  // Determine the fallback URL
  $variables['attributes']['data-fallback-image'] = image_style_url($variables['fallback_style_name'], $original_image);

  return theme('image', $variables);
}

Don't forget to define your theme functions!

In order to use these additional theme functions you need to add a module theme hook specifying the theme functions you need.

/**
 * Implements hook_theme()
 */
function image_fallback_theme($existing, $type, $theme, $path) {

  return array('fallback_image' => array(
      'variables' => array(
        'fallback_style_name' => NULL,
        'style_name' => NULL,
        'path' => NULL,
        'width' => NULL,
        'height' => NULL,
        'alt' => '',
        'title' => NULL,
        'attributes' => array(),
      ),
    ),
    'fallback_image_formatter' => array(
      'variables' => array(
        'item' => NULL, 
        'path' => NULL, 
        'fallback_image_style' => NULL,
        'image_style' => NULL,
      ),
    ),
  );

}

In summary

Hopefully this example led post will give you an idea of how straightforward it is to add your own field formatters to Drupal. This will work for any field type.

The four key field formatter hooks to remember are:

  • info - define the formatter to use
  • settings_form - define the settings form
  • summary - define the summary of the field settings
  • view - define how the field content will output