Creating an image caption with a Field API formatter

Update 10/24/11: This article is the most popular article on my site. To simplify everything, I've implemented this as Image Caption Formatter on drupal.org.

One content type in my current Drupal 7 project contains an image upload field. On display, I need a caption to appear below the image. Quickly three methods come to mind: node template, theme function override (theme_image_formatter()), or the new Field API. Since I've previously done it the other two ways, I decided to use the Field API. It is a bit overkill for this functionality, but I want the experience.

Rather than add a new image type with caption field, I decided to keep it simple and display the image title as the caption. I originally referenced: http://zufelt.ca/blog/how-create-custom-field-drupal-7-field-formatter then later the actual D7 code: http://api.drupal.org/api/drupal/modules--image--image.field.inc/7/source to create the formatter. This code is probably best used in a site specific custom code module.

<?php
/**
 * Implements hook_field_formatter_info().
 */
function imgcaption_formatter_field_formatter_info() {
  return array(
    'image_caption' => array(
      'label' => t('Image with title as caption'),
      'field types' => array('image'),
      'settings' => array('image_style' => '', 'image_link' => ''),
    ),
  );
}

/**
 * Implements hook_field_formatter_settings_summary().
 *
 * Near duplicate of image_field_formatter_settings_summary()
 */
function imgcaption_formatter_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];

  $summary = array();

  $image_styles = image_style_options(FALSE);
  // 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');
  }

  $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);
}

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

  $image_styles = image_style_options(FALSE);
  $element['image_style'] = array(
    '#title' => t('Image style'),
    '#type' => 'select',
    '#default_value' => $settings['image_style'],
    '#empty_option' => t('None (original image)'),
    '#options' => $image_styles,
  );

  $link_types = array(
    'content' => t('Content'),
    'file' => t('File'),
  );
  $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;
}

/**
 * Implements hook_field_formatter_view().
 *
 * Near duplicate of image_field_formatter_view()
 */
function imgcaption_formatter_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();

  // Check if the formatter involves a link.
  if ($display['settings']['image_link'] == 'content') {
    $uri = entity_uri($entity_type, $entity);
  }
  elseif ($display['settings']['image_link'] == 'file') {
    $link_file = TRUE;
  }

  foreach ($items as $delta => $item) {
    if (isset($link_file)) {
      $uri = array(
        'path' => file_create_url($item['uri']),
        'options' => array(),
      );
    }
    $element[$delta] = array(
      '#theme' => 'imgcaption_formatter',
      '#item' => $item,
      '#image_style' => $display['settings']['image_style'],
      '#path' => isset($uri) ? $uri : '',
    );
  }

  return $element;
}


/**
 * Implements hook_theme().
 */
function imgcaption_formatter_theme() {
  return array(
    'imgcaption_formatter' => array(
      'variables' => array('item' => NULL, 'path' => NULL, 'image_style' => NULL),
    ),
  );
}

/**
 * Returns HTML for an image caption field formatter.
 *
 * @param $variables
 *   An associative array containing:
 *   - item: An array of image data.
 *   - image_style: An optional image style.
 *   - path: An array containing the link 'path' and link 'options'.
 *
 * @ingroup themeable
 *
 * Near duplicate of theme_image_formatter()
 */
function theme_imgcaption_formatter($variables) {
  $item = $variables['item'];
  $image = array(
    'path' => $item['uri'],
    'alt' => $item['alt'],
  );
  // Do not output an empty 'title' attribute.
  if (drupal_strlen($item['title']) > 0) {
    $image['title'] = $item['title'];
  }

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

  if ($variables['path']) {
    $path = $variables['path']['path'];
    $options = $variables['path']['options'];
    // When displaying an image inside a link, the html option must be TRUE.
    $options['html'] = TRUE;
    $output = l($output, $path, $options);
  }

  if ($image['title']) {
    $output = imgcaption_formatter_caption($output, $image['title']);
  }
  return $output;
}

function imgcaption_formatter_caption($image, $title) {
  $attributes = array();
  $attributes['class'] = 'imgcaption';

  return $image . '<div ' . drupal_attributes($attributes) . '>' . $title . '</div>';
}

Comments

Thanks for providing this. Where would/could this code be placed in order to display the contents of the "image->title" field in the node? Is this overriding something in the image or field modules? Not sure how to implement this.

I used your formatter, and it worked fine on the field in a node, but the caption doesn't span the width of the image. Where would I modify the css for it to automatically pick up the with of the image, and not have line breaks before the the right edge of the image.

Also, is there a way i can update all my nodes that have an image field using this formatter to a custom title taken from another field or data source?

Thanks.

This is a per content type setting, so changing the option changes it for all nodes of that content type. As soon as I can, I'll be updating the module on d.o to include additional functionality, such as a different data source and auto sizing.

With this code or the dev module, you need to set the <div> to the same size as the image, but that shouldn't be a problem since you should be using an image style.

"With this code or the dev module, you need to set the <div> to the same size as the image, but that shouldn't be a problem since you should be using an image style."

In many use cases, yes. But in the case where you have in image defined with variable sizes defined, this is a problem. I have an Image Style that says, "scale down to be no larger than 200px wide," but nothing stopping the user from uploading an image at 150px wide. In this case, the caption would be 50px too wide. I'm thinking that JavaScript/jQuery is the only way around this.

Set the "Minimum image resolution" in the image field settings at domain.com/admin/structure/types/manage/content_type/fields/field_image

Nice write-up. Thank you. My suggestion is to call the image_field hooks and then modify the results (if needed), rather than copy-and-paste them into your hook functions. Much shorter and more OOP-like.

Yes, I always suggest using the image_field hooks, but this is an example of creating a new image field formatting module.

I found this article whilst looking for a way to add a the image title as a caption in a photo gallery. I subsequently solved it myself using Views, although I plan to try the module referred to at the top of this page to see if it's easier to implement in my use case.

My solution allows you to use the Title field for an image as a caption, and to output this for all of the images in a gallery. No coding required - just the correct configuration of fields in Views.

I usually create a gallery by adding an image field to a content type and allowing multiple (unlimited) images to be uploaded. I turn on both the ALT and Title field options for the image field.

I then create a gallery using Views.

I set a Contextual Filter using the Content ID as a default value. This ensures that only images from the node being displayed are shown in the gallery.

Then, I add the image field from the content type to the view twice and rewrite one of them to output the Title of the image rather than the image itself. Look in the Replacement Patterns to find the correct token - will be something like [field_images_1-title], where images is the field name, _1 indicates I'm using the second instance of the field in the view, and title references the image title field.

I also turn off the "Display all values in the same row" option in Multiple Field Settings for both instances of the field. You'll end up with duplicates otherwise.

This will output each image attached to the node with the caption either above or below the image, depending on which of the two image field instances you rewrote to output the image title.