When #2831937: Add "Image" MediaSource plugin lands in core, and the 8.x-2.x branch of Media Entity Image is opened, we should implement an Image plugin that supports extracting EXIF data, since that functionality has been removed from the core Image plugin: #2831937-87: Add "Image" MediaSource plugin.

Remaining Tasks

CommentFileSizeAuthor
#15 2886552-15.patch863 bytesDaluxz
#7 2886552-7.patch25.92 KBmarcoscano
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

phenaproxima created an issue. See original summary.

phenaproxima’s picture

Wim Leers’s picture

Status: Postponed » Active
Parent issue: » #2831937: Add "Image" MediaSource plugin
jwilson3’s picture

geek-merlin’s picture

Also see the discussion on pluggable metadata via typed data in #2862467: Add complex field mapping to media module.

marcoscano’s picture

Project: Media entity image » Media Entity Image EXIF
Version: 8.x-1.x-dev »
Category: Feature request » Task
Issue tags: +Media Initiative

Moving this to the "Media Entity Image EXIF" module, which is where this code will likely live.

marcoscano’s picture

Status: Active » Needs review
FileSize
25.92 KB

I have opened a -dev branch of this module, and committed the attached patch to allow testing.

After the feedback from https://www.drupal.org/project/media_entity_image/issues/2925712#comment... and from commenting it on slack, I changed the approach to directly hijack the main image source plugin and use our class for it, instead of adding a different plugin, as proposed in https://www.drupal.org/project/media_entity_image/issues/2925712#comment....

Feedback would be very welcome.

SocialNicheGuru’s picture

Status: Needs review » Needs work

On the media image entity that I created I can select 'Yes' for "Whether to gather exif data."
But it is not saved when I hit save.
Instead it reverts to 'No'

glenshewchuck’s picture

Version: » 8.x-1.x-dev

SocialNicheGuru, it looks like the field defined in buildConfigurationForm should have an ajax callback like on the Media Entity Image module. On another site I installed Media Entity and Media Entity Image modules and took a look at the code. I'm not yet sure how to fix.

SocialNicheGuru’s picture

Hmmmm... now i am using core media and not the media_entity module. Does that make a difference?

glenshewchuck’s picture

This module is for core. I was just seeing how it was being done previously and compare the two. I inspected with Firefox to see that the Yes/No in the original media_entity_image module added a javascript event handler that made an ajax call to define the mapping fields. I then looked at this module that is used with core media and saw no event handler so I then looked at the difference in the way each module defined the field.

On the original media_entity_image:

    $form['gather_exif'] = [
      '#type' => 'select',
      '#title' => $this->t('Whether to gather exif data.'),
      '#description' => $this->t('Gather exif data using exif_read_data().'),
      '#default_value' => empty($this->configuration['gather_exif']) || !function_exists('exif_read_data') ? 0 : $this->configuration['gather_exif'],
      '#options' => [
        0 => $this->t('No'),
        1 => $this->t('Yes'),
      ],
      '#ajax' => [
        'callback' => '::ajaxTypeProviderData',
      ],
      '#disabled' => (function_exists('exif_read_data')) ? FALSE : TRUE,
    ];

On this module's field definition there is no #ajax callback

    $form['gather_exif'] = [
      '#type' => 'select',
      '#title' => $this->t('Whether to gather exif data.'),
      '#description' => $this->t('Gather exif data using exif_read_data().'),
      '#default_value' => empty($this->configuration['gather_exif']) || !function_exists('exif_read_data') ? 0 : $this->configuration['gather_exif'],
      '#options' => [
        0 => $this->t('No'),
        1 => $this->t('Yes'),
      ],
      '#disabled' => (function_exists('exif_read_data')) ? FALSE : TRUE,
    ];
sahaj’s picture

@glenshewchuck any idea about how to fix this?

I have naively tried to add the missing

'#ajax' => [
        'callback' => '::ajaxTypeProviderData',
      ], 

code, but of course this do not work.

W01F’s picture

Just checking if any progress had been made on this module? With the new media gallery browser, etc. in core and stable - a reliable, efficient way to add exif data would make album projects incredibly powerful with Drupal.

sahaj’s picture

Honestly, this is one of the most frustrating side of Drupal 8 development:
1. evolving to nice media management
2. but then, not being able to follow with some basic features
3. no feedback on comment since 6 months

Daluxz’s picture

FileSize
863 bytes

In it's current state, it's impossible to turn on exif support on media entities with this module.
The 'Whether to gather exif data' field does not get its data saved, and that makes it impossible to setup the metadata mapping.

Here is a patch that saves the setting for 'yes' or 'no' from the 'Whether to gather exif data' field.
You will need to reload the settings-form for the new mapping options to appear. I tried to change the ajax callback, but couldn't figure out how to fix it, so currently the ajax callback returns an unchanged form.

I also found some other problems, but I will create separate issues for that.

Nick Hope’s picture

I have tried without patch #15, with patch #15, and with both patch #15 and that patch. In each case I cannot select the "Whether to gather exif data." dropdown. It just shows "No". Drupal 9.1.7.

Gather EXIF data

Nick Hope’s picture

It turns out my problem in #16 was because extension=php_exif.dll is commented out by default in Acquia DevDesktop's php.ini file. Apologies for that.

After enabling that, I don't have a problem with saving the Yes/No configuration, without applying any patch. gather_exif: changes between '0' and '1' when I save the form.

There are a number of issues preventing the 8.x-1.x-dev build of the module from functioning nicely, or functioning at all. After applying @Daluxz's patches from #15, and from here, here & here, I have been able to get this working very nicely. As it does not seem to have a future, I have adapted it as a custom module for extracting GPSAltitude as a string, and DateTimeOriginal as a date. (GPSLatitude and GPSLongitude I am extracting with Geocoder). I am posting the code of my ImageWithExif.php file below, leaving in lots of explanatory comments. Perhaps it can help someone else adapt this module for their needs too. The Exif module is an alternative, but it presents read-only fields on entity forms, and I didn't find it as slick as this module, once configured.

<?php

namespace Drupal\media_entity_image_exif\Plugin\media\Source;

use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Form\FormStateInterface;
// Line below added by support-date-fields patch https://www.drupal.org/project/media_entity_image_exif/issues/3031812
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\media\MediaInterface;
use Drupal\media\Plugin\media\Source\Image;

/**
 * Image entity media source, with EXIF-handling capabilities.
 *
 * @see \Drupal\media\Plugin\media\Source\Image
 */
class ImageWithExif extends Image {

  /**
   * The exif data.
   *
   * @var array
   */
  protected $exif;

  /**
   * {@inheritdoc}
   */
  public function getMetadataAttributes() {
    $attributes = parent::getMetadataAttributes();

    $attributes += [
      static::METADATA_ATTRIBUTE_WIDTH => $this->t('Width'),
      static::METADATA_ATTRIBUTE_HEIGHT => $this->t('Height'),
    ];

    if (!empty($this->configuration['gather_exif'])) {
      $attributes += [
        // 'model' => $this->t('Camera model'),
        // 'iso' => $this->t('Iso'),
        // 'exposure' => $this->t('Exposure time'),
        // 'aperture' => $this->t('Aperture value'),
        // 'focal_length' => $this->t('Focal length'),
        // 'created' => $this->t('Image creation datetime'),
        'date_created' => $this->t('Image creation date'),
        'gps_altitude' => $this->t('GPS Altitude'),
      ];
    }

    return $attributes;
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);

    $form['gather_exif'] = [
      '#type' => 'select',
      '#title' => $this->t('Whether to gather exif data.'),
      '#description' => $this->t('Gather exif data using exif_read_data().'),
      '#default_value' => empty($this->configuration['gather_exif']) || !function_exists('exif_read_data') ? 0 : $this->configuration['gather_exif'],
      '#options' => [
        0 => $this->t('No'),
        1 => $this->t('Yes'),
      ],
      //"Currently the ajax callback returns an unchanged form.". See https://www.drupal.org/project/media_entity_image_exif/issues/2886552#comment-12963951
      '#ajax' => [
        'callback' => '::ajaxHandlerData',
      ],
      '#disabled' => (function_exists('exif_read_data')) ? FALSE : TRUE,
    ];

    return $form;
  }

  //Function added to save the setting for 'yes' or 'no' from the 'Whether to gather exif data' field. See https://www.drupal.org/project/media_entity_image_exif/issues/2886552#comment-12963951. The function at the bottom of the file from https://www.drupal.org/project/media_entity_image_exif/issues/3067266 may be an alternative to this.
  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValues();

    if (isset($values['gather_exif'])) {
      $this->configuration['gather_exif'] = $values['gather_exif'];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getMetadata(MediaInterface $media, $name) {
    // Get the file and image data.
    /** @var \Drupal\file\FileInterface $file */
    $file = $media->get($this->configuration['source_field'])->entity;
    // If the source field is not required, it may be empty.
    if (!$file) {
      return parent::getMetadata($media, $name);
    }

    $uri = $file->getFileUri();
    $image = $this->imageFactory->get($uri);
    switch ($name) {
      case static::METADATA_ATTRIBUTE_WIDTH:
        return $image->getWidth() ?: NULL;

      case static::METADATA_ATTRIBUTE_HEIGHT:
        return $image->getHeight() ?: NULL;

      case 'thumbnail_uri':
        return $uri;
    }

    if (!empty($this->configuration['gather_exif']) && function_exists('exif_read_data')) {
      switch ($name) {
        // case 'model':
        //   return $this->getExifField($uri, 'Model');

        // case 'iso':
        //   return $this->getExifField($uri, 'ISOSpeedRatings');

        // case 'exposure':
        //   return $this->getExifField($uri, 'ExposureTime');

        // case 'aperture':
        //   return $this->getExifField($uri, 'FNumber');

        // case 'focal_length':
        //   return $this->getExifField($uri, 'FocalLength');

        // case 'created':
        // $datetime = new DrupalDateTime($this->getExifField($uri, 'DateTimeOriginal'));
        // // The 2 lines below, instead of the one above, support date instead of just string. See https://www.drupal.org/project/media_entity_image_exif/issues/3031812
        // return $date->getTimestamp();
        // return $datetime->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT); //Date and time.

        case 'date_created':
          $date = new DrupalDateTime($this->getExifField($uri, 'DateTimeOriginal'));
          return $date->format(DateTimeItemInterface::DATE_STORAGE_FORMAT); //Date only

        case 'gps_altitude':
          // return $this->getExifField($uri, 'GPSAltitude'); //Original code which returns a fraction
          $value = $this->getExifField($uri, 'GPSAltitude');
          if (strpos($value, '/') !== FALSE) {
            $value = $this->normaliseFraction($value);
          }
          return $value;
      }
    }

    return parent::getMetadata($media, $name);
  }

  /**
   * Get exif field value.
   *
   * @param string $uri
   *   The uri for the file that we are getting the Exif.
   * @param string $field
   *   The name of the exif field.
   *
   * @return string|bool
   *   The value for the requested field or FALSE if is not set.
   */
  protected function getExifField($uri, $field) {
    if (empty($this->exif)) {
      $this->exif = $this->getExif($uri);
    }
    return !empty($this->exif[$field]) ? $this->exif[$field] : FALSE;
  }

  /**
   * Read EXIF.
   *
   * @param string $uri
   *   The uri for the file that we are getting the Exif.
   *
   * @return array|bool
   *   An associative array where the array indexes are the header names and
   *   the array values are the values associated with those headers or FALSE
   *   if the data can't be read.
   */
  protected function getExif($uri) {
    //These 2 lines, instead one of the single lines below them, greatly reduce exif_read_data failures. See https://www.drupal.org/project/media_entity_image_exif/issues/3031810
    $file = \Drupal::service('file_system')->realpath($uri);
    return exif_read_data($file, 'EXIF');
    // return exif_read_data($uri, 'EXIF'); //Permit warnings
    // return @exif_read_data($uri, 'EXIF'); //Hide warnings
  }

  // The function below added by the patch at https://www.drupal.org/project/media_entity_image_exif/issues/3031822
  // I'm only using it for altitude, so it might be OTT. Without it, altitude is shown like 19032/1000
  /**
   * Normalise fractions.
   */
  private function normaliseFraction($fraction) {
  $parts = explode('/', $fraction);
  $top = $parts[0];
  $bottom = $parts[1];

  if ($top > $bottom) {
    // Value > 1.
    if (($top % $bottom) == 0) {
      $value = ($top / $bottom);
    }
    else {
      $value = round(($top / $bottom), 2);
    }
  }
  else {
    if ($top == $bottom) {
      // Value = 1.
      $value = '1';
    }
    else {
      // Value < 1.
      if ($top == 1) {
        $value = '1/' . $bottom;
      }
      else {
        if ($top != 0) {
          $value = '1/' . round(($bottom / $top), 0);
        }
        else {
          $value = '0';
        }
      }
    }
  }
  return $value;
  }

  // // The function below is from the patch at https://www.drupal.org/project/media_entity_image_exif/issues/3067266
  // // Not sure if I need this as well as the additions at lines 68 & 78 from https://www.drupal.org/project/media_entity_image_exif/issues/2886552#comment-12963951. Once a configuration has been saved, this is no longer important.
  // /**
  //  * {@inheritdoc}
  //  */
  // public function defaultConfiguration() {
  //   $parentConfiguration = parent::defaultConfiguration();
  //   $parentConfiguration['gather_exif'] = 0;
  //   return $parentConfiguration;
  // }

}