You are here

imagefield_crop.module in Imagefield Crop 7

Provide a widget to crop uploaded image.

File

imagefield_crop.module
View source
<?php

/**
 * @file
 * Provide a widget to crop uploaded image.
 */

/**
 * Implements hook_field_widget_info().
 */
function imagefield_crop_field_widget_info() {
  return array(
    'imagefield_crop_widget' => array(
      'label' => t('Image with cropping'),
      'field types' => array(
        'image',
      ),
      'settings' => array(
        'progress_indicator' => 'throbber',
        'preview_image_style' => 'thumbnail',
        'collapsible' => 2,
        'resolution' => '200x150',
        'enforce_ratio' => TRUE,
        'enforce_minimum' => TRUE,
        'croparea' => '500x500',
      ),
      'behaviors' => array(
        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
        'default value' => FIELD_BEHAVIOR_NONE,
      ),
    ),
  );
}

/**
 * Get imagefield_crop settings for a file.
 */
function imagefield_crop_file_settings($fid) {
  $settings =& drupal_static(__FUNCTION__, array());
  if (isset($settings[$fid])) {
    return $settings[$fid];
  }
  $settings[$fid] = db_query('SELECT * FROM {imagefield_crop} WHERE fid = :fid', array(
    ':fid' => $fid,
  ))
    ->fetchAssoc();

  // We should always return an array, even if there are no settings found.
  if (!is_array($settings[$fid])) {
    $settings[$fid] = array(
      'x' => 0,
      'y' => 0,
      'width' => 50,
      'height' => 50,
      'changed' => 0,
    );
  }
  return $settings[$fid];
}

/**
 * Save imagefield_crop settings for a file.
 */
function imagefield_crop_file_settings_save($crop_settings) {
  $fid = $crop_settings['fid'];
  $settings =& drupal_static('imagefield_crop_file_settings', array());
  $settings[$fid] = $crop_settings;
  unset($crop_settings['fid']);
  return db_merge('imagefield_crop')
    ->key(array(
    'fid' => $fid,
  ))
    ->fields($crop_settings)
    ->execute();
}

/**
 * Implements hook_field_widget_settings_form().
 */
function imagefield_crop_field_widget_settings_form($field, $instance) {
  $widget = $instance['widget'];
  $settings = $widget['settings'];

  // Use the image widget settings form.
  $form = image_field_widget_settings_form($field, $instance);
  $form['collapsible'] = array(
    '#type' => 'radios',
    '#title' => t('Collapsible behavior'),
    '#options' => array(
      1 => t('None.'),
      2 => t('Collapsible, expanded by default.'),
      3 => t('Collapsible, collapsed by default.'),
    ),
    '#default_value' => $settings['collapsible'],
  );

  // Resolution settings.
  $resolution = explode('x', $settings['resolution']) + array(
    '',
    '',
  );
  $form['resolution'] = array(
    '#title' => t('The resolution to crop the image onto'),
    '#element_validate' => array(
      '_image_field_resolution_validate',
      '_imagefield_crop_widget_resolution_validate',
    ),
    '#theme_wrappers' => array(
      'form_element',
    ),
    '#description' => t('The output resolution of the cropped image, expressed as WIDTHxHEIGHT (e.g. 640x480). Set to 0 not to rescale after cropping. Note: output resolution must be defined in order to present a dynamic preview.'),
  );
  $form['resolution']['x'] = array(
    '#type' => 'textfield',
    '#default_value' => $resolution[0],
    '#size' => 5,
    '#maxlength' => 5,
    '#field_suffix' => ' x ',
    '#theme_wrappers' => array(),
  );
  $form['resolution']['y'] = array(
    '#type' => 'textfield',
    '#default_value' => $resolution[1],
    '#size' => 5,
    '#maxlength' => 5,
    '#field_suffix' => ' ' . t('pixels'),
    '#theme_wrappers' => array(),
  );
  $form['enforce_ratio'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enforce crop box ratio'),
    '#default_value' => $settings['enforce_ratio'],
    '#description' => t('Check this to force the ratio of the output on the crop box. NOTE: If you leave this unchecked but enforce an output resolution, the final image might be distorted'),
    '#element_validate' => array(
      '_imagefield_crop_widget_enforce_ratio_validate',
    ),
  );
  $form['enforce_minimum'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enforce minimum crop size based on the output size'),
    '#default_value' => $settings['enforce_minimum'],
    '#description' => t('Check this to force a minimum cropping selection equal to the output size. NOTE: If you leave this unchecked you might get zoomed pixels if the cropping area is smaller than the output resolution.'),
    '#element_validate' => array(
      '_imagefield_crop_widget_enforce_minimum_validate',
    ),
  );

  // Crop area settings.
  $croparea = explode('x', $settings['croparea']) + array(
    '',
    '',
  );
  $form['croparea'] = array(
    '#title' => t('The resolution of the cropping area'),
    '#element_validate' => array(
      '_imagefield_crop_widget_croparea_validate',
    ),
    '#theme_wrappers' => array(
      'form_element',
    ),
    '#description' => t('The resolution of the area used for the cropping of the image. Image will displayed at this resolution for cropping. Use WIDTHxHEIGHT format, empty or zero values are permitted, e.g. 500x will limit crop box to 500 pixels width.'),
  );
  $form['croparea']['x'] = array(
    '#type' => 'textfield',
    '#default_value' => $croparea[0],
    '#size' => 5,
    '#maxlength' => 5,
    '#field_suffix' => ' x ',
    '#theme_wrappers' => array(),
  );
  $form['croparea']['y'] = array(
    '#type' => 'textfield',
    '#default_value' => $croparea[1],
    '#size' => 5,
    '#maxlength' => 5,
    '#field_suffix' => ' ' . t('pixels'),
    '#theme_wrappers' => array(),
  );
  return $form;
}
function _imagefield_crop_widget_resolution_validate($element, &$form_state) {
  $settings = $form_state['values']['instance']['widget']['settings'];

  // _image_field_resolution_validate() does most of the validation.
  if ($settings['enforce_ratio'] && empty($element['x']['#value'])) {
    form_error($element, t('Target resolution must be defined as WIDTHxHEIGHT if resolution is to be enforced'));
  }
}
function _imagefield_crop_widget_enforce_ratio_validate($element, &$form_state) {
  $settings = $form_state['values']['instance']['widget']['settings'];
  if ($settings['resolution'] && !$element['#value']) {
    drupal_set_message(t('Output resolution is defined, but not enforced. Final images might be distroted'));
  }
}
function _imagefield_crop_widget_enforce_minimum_validate($element, &$form_state) {
  $settings = $form_state['values']['instance']['widget']['settings'];
  list($rw, $rh) = !empty($settings['resolution']) ? explode('x', $settings['resolution']) : array(
    0,
    0,
  );
  if ($settings['enforce_minimum'] && (!is_numeric($rw) || (int) $rw != $rw || $rw <= 0 || !is_numeric($rh) || (int) $rh != $rh || $rh <= 0)) {
    form_error($element, t('Target resolution must be defined as WIDTH_HEIGHT if minimum is to be enforced.'));
  }
}
function _imagefield_crop_widget_croparea_validate($element, &$form_state) {
  foreach (array(
    'x',
    'y',
  ) as $dimension) {
    $value = $element[$dimension]['#value'];
    if (!empty($value) && !is_numeric($value)) {
      form_error($element[$dimension], t('The @dimension value must be numeric.', array(
        '@dimesion' => $dimension,
      )));
      return;
    }
  }
  form_set_value($element, (int) $element['x']['#value'] . 'x' . (int) $element['y']['#value'], $form_state);
}

/**
 * Implements hook_field_widget_form().
 */
function imagefield_crop_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $elements = image_field_widget_form($form, $form_state, $field, $instance, $langcode, $items, $delta, $element);
  foreach (element_children($elements) as $delta) {

    // Add all extra functionality provided by the imagefield_crop widget.
    $elements[$delta]['#process'][] = 'imagefield_crop_widget_process';

    // Register our value callback.
    $elements[$delta]['#file_value_callbacks'] = array_merge(array(
      'imagefield_crop_widget_value',
    ), element_info_property('managed_file', '#file_value_callbacks', array()));
  }
  return $elements;
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Provides option to disable the max width fix.
 */
function imagefield_crop_form_system_themes_admin_form_alter(&$form, &$form_state, $form_id) {
  $form['admin_theme']['imagefield_crop_max_width_fix'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use the max-width fix for imagefield_crop'),
    '#default_value' => variable_get('imagefield_crop_max_width_fix', TRUE),
  );
  $form['#submit'][] = 'imagefield_crop_form_system_themes_admin_form_alter_submit';
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Provides option to disable the max width fix.
 */
function imagefield_crop_form_system_themes_admin_form_alter_submit($form, &$form_state) {
  $imagefield_crop_max_width_fix = isset($form_state['values']['imagefield_crop_max_width_fix']) ? $form_state['values']['imagefield_crop_max_width_fix'] : TRUE;
  variable_set('imagefield_crop_max_width_fix', $imagefield_crop_max_width_fix);
}

/**
 * An element #process callback for the imagefield_crop field type.
 */
function imagefield_crop_widget_process($element, &$form_state, $form) {
  $item = $element['#value'];
  $item['fid'] = $element['fid']['#value'];
  $instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);
  $settings = $instance['settings'];
  $widget_settings = $instance['widget']['settings'];
  $element['#theme'] = 'imagefield_crop_widget';
  $path = drupal_get_path('module', 'imagefield_crop');
  $element['#attached']['js'][] = "{$path}/Jcrop/js/jquery.Jcrop.js";

  // We must define Drupal.behaviors for ahah to work, even if there is no file.
  $element['#attached']['js'][] = "{$path}/imagefield_crop.js";
  $element['#attached']['css'][] = "{$path}/Jcrop/css/jquery.Jcrop.css";

  // Made a configuration since this is something that your local theme might not need.
  if (variable_get('imagefield_crop_max_width_fix', TRUE)) {
    $element['#attached']['css'][] = $path . '/css/imagefield-zenfix.css';
  }
  if ($element['#file']) {
    $file_to_crop = _imagefield_crop_file_to_crop($element['#file']->fid);
    $element['cropinfo'] = _imagefield_add_cropinfo_fields($element['#file']->fid);
    list($res_w, $res_h) = explode('x', $widget_settings['resolution']);
    list($crop_w, $crop_h) = explode('x', $widget_settings['croparea']);
    $element['preview'] = array(
      '#type' => 'markup',
      // This is used by the #process function.
      '#file' => $file_to_crop,
      '#process' => array(
        'imagefield_crop_widget_preview_process',
      ),
      '#theme' => 'imagefield_crop_preview',
      '#description' => t('Image preview <strong>(@res_wpx x @res_hpx)</strong>', array(
        '@res_w' => $res_w,
        '@res_h' => $res_h,
      )),
      '#markup' => theme('image', array(
        'path' => $element['#file']->uri,
        'getsize' => FALSE,
        'attributes' => array(
          'class' => 'preview-existing',
        ),
      )),
    );
    $element['cropbox'] = array(
      '#markup' => theme('image', array(
        'path' => $file_to_crop->uri,
        'attributes' => array(
          'class' => 'cropbox',
          'id' => $element['#id'] . '-cropbox',
        ),
      )),
      '#description' => t('Click on the image and drag to mark how the image will be cropped.'),
    );
    $settings = array(
      $element['#id'] => array(
        'box' => array(
          'ratio' => $res_h ? $widget_settings['enforce_ratio'] * $res_w / $res_h : 0,
          'box_width' => $crop_w,
          'box_height' => $crop_h,
        ),
        'minimum' => array(
          'width' => $widget_settings['enforce_minimum'] ? $res_w : NULL,
          'height' => $widget_settings['enforce_minimum'] ? $res_h : NULL,
        ),
      ),
    );
    $element['#attached']['js'][] = array(
      'data' => array(
        'imagefield_crop' => $settings,
      ),
      'type' => 'setting',
      'scope' => 'header',
    );
  }

  // Prepend submit handler to remove button.
  array_unshift($element['remove_button']['#submit'], 'imagefield_crop_widget_delete');
  return $element;
}
function _imagefield_add_cropinfo_fields($fid = NULL) {
  $defaults = imagefield_crop_file_settings($fid);
  $defaults['changed'] = 0;
  if ($fid) {
    $crop_info = variable_get('imagefield_crop_info', array());
    if (isset($crop_info[$fid]) && !empty($crop_info[$fid])) {
      $defaults = array_merge($defaults, $crop_info[$fid]);
    }
  }
  foreach ($defaults as $name => $default) {
    $element[$name] = array(
      '#type' => 'hidden',
      '#title' => $name,
      '#attributes' => array(
        'class' => array(
          'edit-image-crop-' . $name,
        ),
      ),
      '#default_value' => $default,
    );
  }
  return $element;
}
function imagefield_crop_widget_preview_process($element, &$form_state, $form) {
  $file = $element['#file'];
  if ($file->fid == 0) {
    return $element;
  }

  // The widget belongs to the parent, so we got to find it first.
  $parents = array_slice($element['#array_parents'], 0, -1);
  $parent = drupal_array_get_nested_value($form, $parents);
  $instance = field_widget_instance($parent, $form_state);
  if ($instance['widget']['settings']['resolution']) {
    list($width, $height) = explode('x', $instance['widget']['settings']['resolution']);
  }
  $image_info = image_get_info(drupal_realpath($file->uri));
  $settings = array(
    $parent['#id'] => array(
      'preview' => array(
        'orig_width' => $image_info['width'],
        'orig_height' => $image_info['height'],
        'width' => (int) $width,
        'height' => (int) $height,
      ),
    ),
  );
  $element['#attached']['js'][] = array(
    'data' => array(
      'imagefield_crop' => $settings,
    ),
    'type' => 'setting',
    'scope' => 'header',
  );
  $element['#imagefield_crop'] = array(
    '#file' => $element['#file'],
    '#width' => $width,
    '#height' => $height,
    '#path' => file_create_url($file->uri),
  );
  return $element;
}

/**
 * value callback
 *
 * Registered by imagefield_crop_field_widget_form()
 */
function imagefield_crop_widget_value(&$element, &$input, $form_state) {

  // set $input['fid'] and that will be the value of the element
  if (!empty($input['fid']) && $input['cropinfo']['changed']) {

    // Get crop and scale info.
    $crop = $input['cropinfo'];
    $instance = field_widget_instance($element, $form_state);
    $scale = NULL;
    if ($instance['widget']['settings']['resolution']) {
      list($scale['width'], $scale['height']) = explode('x', $instance['widget']['settings']['resolution']);
    }
    $src = file_load($input['fid']);
    $file_to_crop = _imagefield_crop_file_to_crop($src->fid);

    // Copy the original aside, for future cropping.
    if ($file_to_crop->fid == $src->fid && ($orig_uri = file_unmanaged_copy($src->uri, $src->uri))) {
      $orig = clone $src;
      $orig->fid = 0;
      $orig->uri = $orig_uri;
      $orig->filename = drupal_basename($orig_uri);
      $orig->status = 1;
      $orig = file_save($orig);
      file_usage_add($orig, 'imagefield_crop', 'file', $src->fid);
    }

    // do the crop. @todo check for errors
    // This worked in D6, doesn't work in D7
    // // Save crop data to the database
    // $src->imagefield_crop = array('crop' => $crop);
    // file_save($src);
    if (_imagefield_crop_resize(drupal_realpath($file_to_crop->uri), $crop, $scale, drupal_realpath($src->uri))) {

      // Insert crop info for this image in imagefield_crop_info variable.
      $crop['fid'] = $src->fid;
      unset($crop['changed']);
      imagefield_crop_file_settings_save($crop);

      // Remove cached versions of the cropped image.
      image_path_flush($src->uri);
    }
  }
}
function imagefield_crop_widget_delete($form, &$form_state) {
  $parents = array_slice($form_state['triggering_element']['#array_parents'], 0, -1);
  $element = drupal_array_get_nested_value($form_state['values'], $parents);
  $orig = _imagefield_crop_file_to_crop($element['fid']);
  if ($orig->fid != $element['fid']) {
    file_usage_delete($orig, 'imagefield_crop');
    file_delete($orig);
    db_delete('imagefield_crop')
      ->condition('fid', $element['fid'])
      ->execute();
  }
}

/**
 * Implements hook_entity_presave().
 */
function imagefield_crop_entity_presave($entity, $type) {

  // HACK - until http://drupal.org/node/1448124 is addressed, the iamgesize
  // is stored in the field instance and we have no way of cleanly altering
  // it correctly, so do it here.
  $fields = field_info_instances($type, isset($entity->type) ? $entity->type : NULL);
  if (isset($entity->type)) {
    _imagefield_crop_entity_presave($entity, $fields);
  }
  else {
    foreach ($fields as $bundle) {
      _imagefield_crop_entity_presave($entity, $bundle);
    }
  }
}

/**
 * Helper function to reset the width and height of the imagefield to
 * those of the cropped image.
 */
function _imagefield_crop_entity_presave($entity, $fields) {
  foreach ($fields as $field_name => $field) {
    if ($field['widget']['type'] !== 'imagefield_crop_widget') {
      continue;
    }
    if (empty($entity->{$field_name})) {
      continue;
    }

    // Get the language key for the field, if fields language key is not
    // $entity->language set $lang to LANGUAGE_NONE.
    $lang = LANGUAGE_NONE;
    if (isset($entity->language, $entity->{$field_name}[$entity->language])) {
      $lang = $entity->language;
    }
    elseif (empty($entity->{$field_name}[$lang])) {
      continue;
    }
    foreach ($entity->{$field_name}[$lang] as $delta => $imagefield) {
      $file = file_load($imagefield['fid']);
      $info = image_get_info($file->uri);
      if (is_array($info)) {
        $entity->{$field_name}[$lang][$delta]['width'] = $info['width'];
        $entity->{$field_name}[$lang][$delta]['height'] = $info['height'];
      }
    }
  }
}

/**
 * Implements hook_theme().
 */
function imagefield_crop_theme() {
  return array(
    'imagefield_crop_widget' => array(
      'render element' => 'element',
      'file' => 'imagefield_crop.theme.inc',
    ),
    'imagefield_crop_preview' => array(
      'render element' => 'element',
      'file' => 'imagefield_crop.theme.inc',
    ),
  );
}

/**
 * Crop the image and resize it.
 */
function _imagefield_crop_resize($src, $crop = NULL, $scale = NULL, $dst = NULL) {
  $image = image_load($src);
  if ($image) {
    $data = array(
      'image' => &$image,
      'crop' => &$crop,
      'scale' => &$scale,
      'src' => &$src,
      'dst' => &$dst,
    );
    drupal_alter('imagefield_crop_resize', $data);
    $result = TRUE;
    if ($crop) {
      $result = $result && image_crop($image, $crop['x'], $crop['y'], $crop['width'], $crop['height']);
    }
    if ($scale) {
      $result = $result && image_scale_and_crop($image, $scale['width'], $scale['height']);
    }
    $result = $result && image_save($image, $dst ? $dst : $src);
    return $result;
  }
  return FALSE;
}
function _imagefield_crop_file_to_crop($fid) {

  // Try to find the original file for this image.
  $result = db_select('file_usage', 'fu')
    ->fields('fu', array(
    'fid',
  ))
    ->condition('module', 'imagefield_crop')
    ->condition('type', 'file')
    ->condition('id', $fid)
    ->condition('count', 0, '>')
    ->range(0, 1)
    ->execute();
  if ($row = $result
    ->fetch()) {
    $fid = $row->fid;
  }
  return file_load($fid);
}