You are here

exif_custom.module in EXIF Custom 7

Primary hook implementations for EXIF Custom.

File

exif_custom.module
View source
<?php

/**
 * @file
 * Primary hook implementations for EXIF Custom.
 */

/**
 * Implements hook_menu().
 */
function exif_custom_menu() {
  $items['admin/config/media/exif_custom'] = array(
    'type' => MENU_NORMAL_ITEM,
    'title' => 'Custom EXIF Mappings',
    'description' => 'Customise mappings of EXIF data',
    'page callback' => 'exif_custom_mappings',
    'access callback' => 'exif_custom_main_page_access',
    'file' => 'exif_custom.pages.inc',
  );
  $items['admin/config/media/exif_custom/maps'] = array(
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'title' => 'Custom EXIF Mappings',
    'description' => 'Customise mappings of EXIF data',
    'page callback' => 'exif_custom_mappings',
    'access callback' => 'exif_custom_main_page_access',
    'file' => 'exif_custom.pages.inc',
  );
  $items['admin/config/media/exif_custom/add'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'New EXIF Mapping',
    'description' => 'Customise mappings of EXIF data',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'exif_custom_new_map_form',
    ),
    'access arguments' => array(
      'administer exif_custom',
    ),
    'file' => 'exif_custom.add.inc',
  );
  $items['admin/config/media/exif_custom/map/%'] = array(
    'type' => MENU_NORMAL_ITEM,
    // @todo Title callback?
    'title' => 'Edit mapping',
    'description' => 'Customise mappings of EXIF data',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'exif_custom_map_edit_form',
    ),
    'access arguments' => array(
      'administer exif_custom',
    ),
    'file' => 'exif_custom.edit.inc',
  );
  $items['admin/config/media/exif_custom/settings'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Settings',
    'description' => 'Customise general settings for EXIF import',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'exif_custom_settings_form',
    ),
    'access arguments' => array(
      'administer exif_custom',
    ),
    'file' => 'exif_custom.settings.inc',
  );
  $items['admin/config/media/exif_custom/user'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'User Settings',
    'description' => 'Customise general settings for EXIF import',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'exif_custom_settings_form_user',
    ),
    'access arguments' => array(
      'have default image metadata profile',
    ),
    'file' => 'exif_custom.user.inc',
  );
  return $items;
}

/**
 * Access callback for the main admin page.
 */
function exif_custom_main_page_access() {
  if (user_access('view image metadata')) {
    return TRUE;
  }
  elseif (user_access('administer exif_custom')) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Implements hook_permission().
 */
function exif_custom_permission() {
  return array(
    'have default image metadata profile' => array(
      'title' => t('Have default image metadata profile'),
      'description' => t('Allow users to override teh site default image metadata profile.'),
    ),
    'view image metadata' => array(
      'title' => t('View image metadata'),
      'description' => t('See what image metdata import profiles have been created.'),
    ),
    'administer exif_custom' => array(
      'title' => 'Administer image metadata',
      'description' => t('Administer image metadata import profiles'),
    ),
  );
}

/**
 * Load a user's custom settings ID.
 */
function exif_custom_get_user_default() {
  global $user;
  $mid = db_query('SELECT mid FROM {exif_custom_users} WHERE uid = :uid', array(
    ':uid' => $user->uid,
  ))
    ->fetchField();
  if (!empty($mid)) {
    return $mid;
  }
  else {
    return '';
  }
}

/**
 * Get a list of custom maps.
 *
 * @return object|bool
 *   Either the list of mappings, or FALSE if none available.
 */
function _exif_custom_get_maps() {
  $results = db_query("SELECT mid, name FROM {exif_custom_maps};");
  if ($results
    ->rowCount() > 0) {
    return $results;
  }
  else {
    return FALSE;
  }
}

/**
 * Implements hook_entity_presave().
 */
function exif_custom_entity_presave($entity, $entity_type) {
  if ($entity_type == 'file') {
    exif_custom_process_entity($entity);
  }
}

/**
 * Process a given entity to see if it has EXIF data to be saved.
 *
 * @param object $file
 *   The file being processed.
 */
function exif_custom_process_entity(&$file) {
  if (!isset($file->type) || !isset($file->uri)) {
    return;
  }
  if ($file->type != 'image') {
    return;
  }
  if (arg(3) == 'exif_custom') {
    return;
  }

  // Media Browser saves in two steps: first a temporary file is saved after
  // which the user is presented with a form an can edit fields on the file.
  // After potentially updating the file fields the user presses save and the
  // file is made permanent and saved. As exif_custom_process_entity is invoked
  // in both situations we need to detect the last save and avoid mapping as it
  // would overwrite any changes the user made.
  if (arg(0) == 'media' && arg(1) == 'browser' && $file->status == FILE_STATUS_PERMANENT) {
    return;
  }

  // Skip mapping if the file has a path that is excluded.
  $patterns = variable_get('exif_custom_exclude_paths', NULL);
  if (!empty($patterns) && drupal_match_path($file->uri, $patterns)) {
    return;
  }
  $data = exif_custom_get_exif_fields($file->uri, TRUE);
  if (empty($data)) {
    return;
  }
  $mappings = exif_custom_get_mapping();
  if (empty($mappings)) {
    return;
  }
  $lang = entity_language('file', $file);
  if (!$lang) {
    $lang = LANGUAGE_NONE;
  }
  foreach ($mappings as $field => $values) {

    // Ignore fields that aren't present.
    if (!isset($data[$values->exif_field])) {
      continue;
    }

    // File name is special case.
    if ($field == 'filename') {
      $file->filename = $data[$values->exif_field];
      continue;
    }

    // Optionally process fields which have already been imported. This avoids
    // re-processing files which have already been saved.
    if (!variable_get('exif_custom_overwrite_existing') && !empty($file->{$field})) {
      continue;
    }
    $array = array();
    $field_info = field_info_field($field);
    $instance_info = field_info_instance('file', $field, 'image');
    switch ($field_info['type']) {
      case 'taxonomy_term_reference':
        $vids = array();
        foreach ($field_info['settings']['allowed_values'] as $allowed_value) {
          $vocabulary = taxonomy_vocabulary_machine_name_load($allowed_value['vocabulary']);
          if ($allowed_value['vocabulary'] === 0) {
            break;
          }
          $vids[$vocabulary->vid] = $vocabulary;
        }
        $element = array();
        $element['#value'] = isset($data[$values->exif_field]) ? $data[$values->exif_field] : FALSE;
        $form_state = array();
        $array = array();
        $array[$lang] = _exif_custom_taxonomy_autocomplete_validate($element, $vids);
        break;
      case 'country':
        $countries = array_flip(countries_get_countries('name'));
        if (isset($countries[$data[$values->exif_field]])) {
          $array[$lang][0]['iso2'] = $countries[$data[$values->exif_field]];
        }
        break;
      case 'creative_commons':
        module_load_include('module', 'creative_commons');
        $licence_by_id = array(
          'CC_NONE' => 1,
          'CC_BY' => 2,
          'CC_BY_SA' => 3,
          'CC_BY_ND' => 4,
          'CC_BY_NC' => 5,
          'CC_BY_NC_SA' => 6,
          'CC_BY_NC_ND' => 7,
          'CC_0' => 8,
          'CC_PD' => 9,
        );
        $licence_by_id = array_merge($licence_by_id, array_flip(creative_commons_get_licence_types()));
        if (isset($licence_by_id[$data[$values->exif_field]])) {
          $array[$lang]['0']['licence'] = $licence_by_id[$data[$values->exif_field]];
        }
        break;
      case 'date':
        $array[$lang]['0']['value'] = strtotime($data[$values->exif_field]);
        break;
      default:
        $array[$lang]['0']['value'] = $data[$values->exif_field];
    }

    // Set the values.
    $file->{$field} = $array;
  }
}

/**
 * Load a mapping for the current user, or the default if not set.
 *
 * @return array
 *   The appropriate mapping for this user.
 */
function exif_custom_get_mapping() {

  // First try and get users default.
  $mid = exif_custom_get_user_default();

  // Use site default or item 0.
  if (empty($mid)) {
    $mid = variable_get('exif_custom_default', 0);
  }
  $return = db_query("SELECT * FROM {exif_custom_mapped_fields}\n    WHERE mid = :mid AND img_field != 'none'", array(
    ':mid' => $mid,
  ))
    ->fetchAllAssoc('img_field');
  return $return;
}

/**
 *
 *
 * @param string $uri
 *   A file URL.
 * @param bool $concatenate_arrays
 *   Whether or not to concatenate values or overwrite them.
 *
 * @return array
 *   The fields extracted.
 */
function exif_custom_get_exif_fields($uri, $concatenate_arrays = TRUE) {

  // Generate the absolute URL for the image just once.
  if (variable_get('exif_custom_request_method', 'legacy') == 'legacy') {
    $image_url = drupal_realpath($uri);
  }
  else {
    $image_url = file_create_url($uri);
  }
  $fields = array();

  // EXIF. Because there can be corrupt / incompatible data in the image, and
  // it doesn't support stream options, throw away any error messages.
  $exif = @exif_read_data($image_url, NULL, TRUE);
  if (isset($exif) && is_array($exif)) {
    foreach ($exif as $name => $section) {
      foreach ($section as $key => $value) {
        if ($concatenate_arrays && is_array($value)) {
          $value = implode(', ', $value);
        }
        $fields['EXIF:' . $name . ':' . $key] = exif_custom_check_plain($value);
      }
    }
  }

  // XMP.
  $fields = array_merge($fields, exif_custom_get_xmp($image_url));

  // IPTC. This function doesn't support stream options so ignore any errors.
  $size = @getimagesize($image_url, $info);
  if (isset($info) && is_array($info)) {
    foreach ($info as $block) {
      $iptc = iptcparse($block);
      if (!empty($iptc)) {

        // IPTC:2#254 can contain name=value pairs.
        if (isset($iptc['2#254']) && is_array($iptc['2#254'])) {
          $i = 0;
          foreach ($iptc['2#254'] as $iptc_field) {
            $subfields = explode('=', $iptc_field);
            $iptc['2#254.' . $subfields[0]] = $subfields[1];
          }
          unset($iptc['2#254']);
        }
        foreach ($iptc as $key => $value) {
          if ($concatenate_arrays && is_array($value)) {
            $value = implode(', ', $value);
          }
          $fields['IPTC:' . $key] = exif_custom_check_plain($value);
        }
      }
    }
  }
  return $fields;
}

/**
 *
 */
function _exif_custom_taxonomy_autocomplete_validate(&$element, $vocabularies) {

  // Autocomplete widgets do not send their tids in the form, so we must detect
  // them here and process them independently.
  $items = array();
  if ($tags = $element['#value']) {

    // Translate term names into actual terms.
    if (is_array($tags)) {

      // If #value is an array, it was probably a bag in the XMP, thus no need
      // to run through drupal_explode_tags().
      $typed_terms = $tags;
    }
    else {
      $typed_terms = drupal_explode_tags($tags);
    }
    foreach ($typed_terms as $typed_term) {

      // See if the term exists in the chosen vocabulary and return the tid;
      // otherwise, create a new 'autocreate' term for insert/update.
      if ($possibilities = taxonomy_term_load_multiple(array(), array(
        'name' => trim($typed_term),
        'vid' => array_keys($vocabularies),
      ))) {
        $term = array_pop($possibilities);
      }
      else {
        $vocabulary = reset($vocabularies);
        $term = array(
          'tid' => 'autocreate',
          'vid' => $vocabulary->vid,
          'name' => $typed_term,
          'vocabulary_machine_name' => $vocabulary->machine_name,
        );
      }
      $items[] = (array) $term;
    }
  }
  foreach ($items as $delta => $item) {
    if ($item['tid'] == 'autocreate') {
      $term = (object) $item;
      unset($term->tid);
      taxonomy_term_save($term);
      $items[$delta]['tid'] = $term->tid;
    }
  }
  return $items;
}

/**
 *
 */
function exif_custom_check_plain($text) {
  if (is_null($text)) {
    $text = "";
  }
  if (!mb_detect_encoding($text, 'UTF-8', TRUE)) {
    $text = html_entity_decode($text);
    $text = mb_convert_encoding($text, 'UTF-8', 'ISO-8859-1');
    $text = str_replace('>', '&gt;', $text);
    $text = str_replace('<', '&lt;', $text);
    $text = str_replace('&lt;br /&gt;', '\\n', $text);
  }
  return $text;

  // Removed as it stops italics in descriptions.
  // return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}

/**
 *
 */
function exif_custom_get_xmp($image) {
  $content = file_get_contents($image);
  $xmp_data_start = strpos($content, '<x:xmpmeta');
  $xmp_data_end = strpos($content, '</x:xmpmeta>');
  if ($xmp_data_start === FALSE || $xmp_data_end === FALSE) {
    return array();
  }
  $xmp_length = $xmp_data_end - $xmp_data_start;
  $xmp_data = substr($content, $xmp_data_start, $xmp_length + 12);
  unset($content);
  $xmp = simplexml_load_string($xmp_data);
  if ($xmp === FALSE) {
    return array();
  }

  // $namespaces = $xmp->getDocNamespaces(true);
  // $fields = array();
  // foreach ($namespaces as $namespace) {
  // $fields[] = exif_custom_xml_recursion($xmp->children($namespace));
  $field_data = array();
  exif_custom_xml_recursion($xmp, $field_data, 'XMP');
  return $field_data;
}

/**
 *
 *
 * @param object $obj
 * @param array $fields
 * @param string name
 *
 * @return array
 */
function exif_custom_xml_recursion($obj, array &$fields, $name) {
  $namespace = $obj
    ->getDocNamespaces(TRUE);
  $namespace[NULL] = NULL;
  $children = array();
  $attributes = array();
  $text = trim((string) $obj);
  if (strlen($text) === 0) {
    $text = NULL;
  }
  if (strtolower((string) $obj
    ->getName()) == "bag") {

    // @todo Add support for bags of objects other than just text?
    $childValues = array();
    $objChildren = $obj
      ->children("rdf", TRUE);
    foreach ($objChildren as $child) {
      $childValues[] = trim((string) $child);
    }
    if (count($childValues) > 0) {
      $fields[$name] = $childValues;
    }
  }
  else {
    $name = $name . ':' . strtolower((string) $obj
      ->getName());

    // Get info for all namespaces.
    if (is_object($obj)) {
      foreach ($namespace as $ns => $nsUrl) {

        // Attributes.
        $objAttributes = $obj
          ->attributes($ns, TRUE);
        foreach ($objAttributes as $attributeName => $attributeValue) {
          $attribName = strtolower(trim((string) $attributeName));
          $attribVal = trim((string) $attributeValue);
          if (!empty($ns)) {
            $attribName = $ns . ':' . $attribName;
          }
          $attributes[$attribName] = $attribVal;
        }

        // Children.
        $objChildren = $obj
          ->children($ns, TRUE);
        foreach ($objChildren as $childName => $child) {
          $childName = strtolower((string) $childName);
          if (!empty($ns)) {
            $childName = $ns . ':' . $childName;
          }
          $children[$childName][] = exif_custom_xml_recursion($child, $fields, $name);
        }
      }
    }
    if (!is_null($text)) {
      $fields[$name] = $text;
    }
  }
  return array(
    'name' => $name,
    'text' => html_entity_decode($text),
    'attributes' => $attributes,
    'children' => $children,
  );
}

/**
 * Get the mapping ID from a Mapping name.
 *
 * @param string $name
 *   The name of the mapping.
 *
 * @return int
 *   The ID, or if not found -1.
 */
function exif_custom_get_mid($name) {
  $mid = db_select('exif_custom_maps', 'm')
    ->fields('m', array(
    'mid',
  ))
    ->condition('m.name', $name)
    ->execute()
    ->fetchField();
  if (!empty($mid)) {
    return $mid;
  }
  return -1;
}

Functions

Namesort descending Description
exif_custom_check_plain
exif_custom_entity_presave Implements hook_entity_presave().
exif_custom_get_exif_fields
exif_custom_get_mapping Load a mapping for the current user, or the default if not set.
exif_custom_get_mid Get the mapping ID from a Mapping name.
exif_custom_get_user_default Load a user's custom settings ID.
exif_custom_get_xmp
exif_custom_main_page_access Access callback for the main admin page.
exif_custom_menu Implements hook_menu().
exif_custom_permission Implements hook_permission().
exif_custom_process_entity Process a given entity to see if it has EXIF data to be saved.
exif_custom_xml_recursion
_exif_custom_get_maps Get a list of custom maps.
_exif_custom_taxonomy_autocomplete_validate