diff --git modules/image/image.admin.css modules/image/image.admin.css new file mode 100644 index 0000000..c279458 --- /dev/null +++ modules/image/image.admin.css @@ -0,0 +1,61 @@ +/* $Id$ */ + +/** + * Image style configuration pages. + */ +div.image-style-new, +div.image-style-new div { + display: inline; +} +div.image-style-preview div.preview-image-wrapper { + float: left; + padding-bottom: 2em; + text-align: center; + top: 50%; + width: 48%; +} +div.image-style-preview div.preview-image { + margin: auto; + position: relative; +} +div.image-style-preview div.preview-image div.width { + border: 1px solid #666; + border-top: none; + height: 2px; + left: -1px; + bottom: -6px; + position: absolute; +} +div.image-style-preview div.preview-image div.width span { + position: relative; + top: 4px; +} +div.image-style-preview div.preview-image div.height { + border: 1px solid #666; + border-left: none; + position: absolute; + right: -6px; + top: -1px; + width: 2px; +} +div.image-style-preview div.preview-image div.height span { + height: 2em; + left: 10px; + margin-top: -1em; + position: absolute; + top: 50%; +} + +/** + * Image anchor element. + */ +table.image-anchor { + width: auto; +} +table.image-anchor tr.even, +table.image-anchor tr.odd { + background: none; +} +table.image-anchor td { + border: 1px solid #CCC; +} diff --git modules/image/image.admin.inc modules/image/image.admin.inc new file mode 100644 index 0000000..9780d30 --- /dev/null +++ modules/image/image.admin.inc @@ -0,0 +1,793 @@ + theme('image_style_list', $styles), + '#attached_css' => array(drupal_get_path('module', 'image') . '/image.admin.css' => array('preprocess' => FALSE)), + ); + + return $page; + +} + +/** + * Menu title callback; Title for editing and deleting styles. + */ +function image_style_title($string, $style) { + return t($string, array('!name' => $style['name'])); +} + +/** + * Form builder; Edit a style name and effect order. + * + * @param $form_state + * An associative array containing the current state of the form. + * @param $style + * An image style array. + * @ingroup forms + * @see image_style_form_submit() + * @see image_style_name_validate() + */ +function image_style_form(&$form_state, $style) { + $form_state['image_style'] = $style; + $form = array( + '#tree' => TRUE, + '#attached_css' => array(drupal_get_path('module', 'image') . '/image.admin.css' => array('preprocess' => FALSE)), + ); + + $form['name'] = array( + '#type' => 'textfield', + '#size' => '64', + '#title' => t('Image style name'), + '#default_value' => $style['name'], + '#description' => t('The name is used in URLs for generated images. Use only lowercase alphanumeric characters, underscores (_), and hyphens (-).'), + '#element_validate' => array('image_style_name_validate'), + ); + + $form['effects'] = array( + '#theme' => 'image_style_effects', + ); + + foreach ($style['effects'] as $ieid => $effect) { + $form['effects'][$ieid]['#weight'] = isset($form_state['input']['effects']) ? $form_state['input']['effects'][$ieid]['weight'] : NULL; + $form['effects'][$ieid]['label'] = array( + '#markup' => $effect['label'], + ); + $form['effects'][$ieid]['summary'] = array( + '#markup' => isset($effect['summary theme']) ? theme($effect['summary theme'], $effect['data']) : '', + ); + $form['effects'][$ieid]['weight'] = array( + '#type' => 'weight', + '#default_value' => $effect['weight'], + ); + $form['effects'][$ieid]['configure'] = array( + '#markup' => isset($effect['form callback']) ? l(t('configure'), 'admin/settings/image-styles/edit/'. $style['name'] .'/effects/'. $effect['ieid'] ) : '', + ); + $form['effects'][$ieid]['remove'] = array( + '#markup' => l(t('delete'), 'admin/settings/image-styles/edit/'. $style['name'] .'/effects/'. $effect['ieid'] .'/delete'), + ); + } + + $new_effect_options = array('' => t('Select a new effect')); + foreach (image_effect_definitions() as $effect => $definition) { + $new_effect_options[$effect] = check_plain($definition['label']); + } + $form['effects']['new'] = array( + '#tree' => FALSE, + '#weight' => isset($form_state['input']['weight']) ? $form_state['input']['weight'] : NULL, + ); + $form['effects']['new']['new'] = array( + '#type' => 'select', + '#options' => $new_effect_options, + ); + $form['effects']['new']['weight'] = array( + '#type' => 'weight', + '#default_value' => count($form['effects']) - 1, + ); + $form['effects']['new']['add'] = array( + '#type' => 'submit', + '#value' => t('Add'), + '#validate' => array('image_style_form_add_validate'), + '#submit' => array('image_style_form_submit', 'image_style_form_add_submit'), + ); + + $form['preview'] = array( + '#type' => 'item', + '#title' => t('Preview'), + '#markup' => theme('image_style_preview', $style), + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Update style'), + ); + + return $form; +} + +/** + * Validate handler for adding a new effect to a style. + */ +function image_style_form_add_validate($form, &$form_state) { + if (!$form_state['values']['new']) { + form_error($form['effects']['new']['new'], t('Select an effect to add.')); + $form_state['rebuild'] = TRUE; + } +} + +/** + * Submit handler for adding a new effect to a style. + */ +function image_style_form_add_submit($form, &$form_state) { + $style = $form_state['image_style']; + // Check if this field has any configuration options. + $effect = image_effect_definition_load($form_state['values']['new']); + + // Load the configuration form for this option. + if (isset($effect['form callback'])) { + $path = 'admin/settings/image-styles/edit/' . $form_state['image_style']['name'] . '/add/' . $form_state['values']['new']; + $form_state['redirect'] = array($path, array('weight' => $form_state['values']['weight'])); + } + // If there's no form, immediately add the effect. + else { + $effect['isid'] = $style['isid']; + $effect['weight'] = $form_state['values']['weight']; + image_effect_save($effect); + drupal_set_message(t('The effect was successfully applied.')); + } +} + +/** + * Submit handler for saving an image style. + */ +function image_style_form_submit($form, &$form_state) { + // Update the style name if it has changed. + $style = $form_state['image_style']; + if ($style['name'] != $form_state['values']['name']) { + $style['name'] = $form_state['values']['name']; + } + + // Update effect weights. + if (!empty($form_state['values']['effects'])) { + foreach ($form_state['values']['effects'] as $ieid => $effect_data) { + if (isset($style['effects'][$ieid])) { + $effect = $style['effects'][$ieid]; + $effect['weight'] = $effect_data['weight']; + image_effect_save($effect); + } + } + } + + image_style_save($style); + if ($form_state['values']['op'] == t('Update style')) { + drupal_set_message('Changes to the style have been saved.'); + } + $form_state['redirect'] = 'admin/settings/image-styles/edit/' . $style['name']; +} + +/** + * Form builder; Form for adding a new style. + * + * @ingroup forms + * @see image_style_add_form_submit() + * @see image_style_name_validate() + */ +function image_style_add_form(&$form_state) { + $form = array(); + + $form['name'] = array( + '#type' => 'textfield', + '#size' => '64', + '#title' => t('Style name'), + '#default_value' => '', + '#description' => t('The name is used in URLs for generated images. Use only lowercase alphanumeric characters, underscores (_), and hyphens (-).'), + '#element_validate' => array('image_style_name_validate'), + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Create new style'), + ); + + return $form; +} + +/** + * Submit handler for adding a new style. + */ +function image_style_add_form_submit($form, &$form_state) { + $style = array('name' => $form_state['values']['name']); + $style = image_style_save($style); + drupal_set_message(t('Style %name was created.', array('%name' => $style['name']))); + $form_state['redirect'] = 'admin/settings/image-styles/edit/'. $style['name']; +} + +/** + * Element validate function to ensure unique, URL safe style names. + */ +function image_style_name_validate($element, $form_state) { + // Check for duplicates. + $styles = image_styles(); + if (isset($styles[$element['#value']]) && (!isset($form_state['image_style']['isid']) || $styles[$element['#value']]['isid'] != $form_state['image_style']['isid'])) { + form_set_error($element['#name'], t('The image style name %name is already in use.', array('%name' => $element['#value']))); + } + + // Check for illegal characters in style names. + if (preg_match('/[^0-9a-z_\-]/', $element['#value'])) { + form_set_error($element['#name'], t('Please only use lowercase alphanumeric characters, underscores (_), and hyphens (-) for style names.')); + } +} + +/** + * Form builder; Form for deleting an image style. + * + * @param $style + * An image style array. + * + * @ingroup forms + * @see image_style_delete_form_submit() + */ +function image_style_delete_form($form_state, $style) { + $form_state['image_style'] = $style; + $form = array(); + + $replacement_styles = array_diff_key(image_style_options(), array($style['name'] => '')); + $replacement_styles[''] = t('No replacement, just delete'); + $form['replacement'] = array( + '#title' => t('Replacement style'), + '#type' => 'select', + '#options' => $replacement_styles, + ); + + return confirm_form( + $form, + t('Optionally select a style before deleting %style', array('%style' => $style['name'])), + 'admin/settings/image-styles', + t('If this style is in use on the site, you may select another style to replace it. All images that have been generated for this style will be permanently deleted.'), + t('Delete'), t('Cancel') + ); +} + +/** + * Submit handler to delete an image style. + */ +function image_style_delete_form_submit($form, &$form_state) { + $style = $form_state['image_style']; + + image_style_delete($style, $form_state['values']['replacement']); + drupal_set_message(t('Style %name was deleted.', array('%name' => $style['name']))); + $form_state['redirect'] = 'admin/settings/image-styles'; +} + +/** + * Menu title callback; Title for editing, deleting, and adding image effects. + * + * @param $effect + * An image effect array. + */ +function image_effect_title($string, $effect) { + return t($string, array('!label' => $effect['label'])); +} + +/** + * Form builder; Form for adding and editing image effects. + * + * This form is used universally for editing all effects. Each effect adds its + * own custom section to the form by calling the form function specified in + * hook_image_effects(). + * + * @param $form_state + * An associative array containing the current state of the form. + * @param $style + * An image style array. + * @param $effect + * An image effect array. + * + * @ingroup forms + * @see hook_image_effects() + * @see image_effects() + * @see image_resize_form() + * @see image_scale_form() + * @see image_rotate_form() + * @see image_crop_form() + * @see image_effect_form_submit() + */ +function image_effect_form(&$form_state, $style, $effect) { + $form_state['image_style'] = $style; + $form_state['image_effect'] = $effect; + + // If there's no configuration for this effect redirect to the effect list. + if (!isset($effect['form callback'])) { + drupal_goto('admin/settings/image-styles/edit/' . $style['name']); + } + + $form = array( + '#tree' => TRUE, + '#attached_css' => array(drupal_get_path('module', 'image') . '/image.admin.css' => array('preprocess' => FALSE)), + ); + if (drupal_function_exists($effect['form callback'])) { + $form['data'] = call_user_func($effect['form callback'], $effect['data']); + } + + // Check if the URL provides a weight, then the effect, otherwise use default. + $form['weight'] = array( + '#type' => 'hidden', + '#value' => isset($_GET['weight']) ? intval($_GET['weight']) : (isset($effect['weight']) ? $effect['weight'] : count($style['effects'])), + ); + + $form['buttons'] = array('#tree' => FALSE); + $form['buttons']['submit'] = array( + '#type' => 'submit', + '#value' => isset($effect['ieid']) ? t('Update effect') : t('Add effect'), + ); + $form['buttons']['cancel'] = array( + '#markup' => l(t('Cancel'), 'admin/settings/image-styles/edit/' . $style['name']), + ); + + return $form; +} + +/** + * Submit handler for updating an image effect. + */ +function image_effect_form_submit($form, &$form_state) { + $style = $form_state['image_style']; + $effect = array_merge($form_state['image_effect'], $form_state['values']); + $effect['isid'] = $style['isid']; + image_effect_save($effect); + drupal_set_message(t('The effect was successfully applied.')); + $form_state['redirect'] = 'admin/settings/image-styles/edit/'. $style['name']; +} + +/** + * Form builder; Form for deleting an image effect. + * + * @param $style + * Name of the style to remove the effect from. + * @param $effect + * Name of the effect to remove. + * @ingroup forms + * @see image_effect_delete_form_submit() + */ +function image_effect_delete_form(&$form_state, $style, $effect) { + $form_state['image_style'] = $style; + $form_state['image_effect'] = $effect; + + $form = array(); + $question = t('Are you sure you want to delete the @effect effect from the %style style?', array('%style' => $style['name'], '@effect' => $effect['label'])); + $description = t('Deleting this effect will regenerate all images for the %style style.', array('%style' => $style['name'])); + return confirm_form($form, $question, 'admin/settings/image-styles/edit/' . $style['name'], $description, t('Delete')); +} + +/** + * Submit handler to delete an image effect. + */ +function image_effect_delete_form_submit($form, &$form_state) { + $style = $form_state['image_style']; + $effect = $form_state['image_effect']; + + image_effect_delete($effect); + drupal_set_message(t('The image effect %name has been deleted.', array('%name' => $effect['label']))); + $form_state['redirect'] = 'admin/settings/image-styles/edit/'. $style['name']; +} + +/** + * Element validate handler to ensure an integer pixel value. + * + * The property #allow_negative = TRUE may be set to allow negative integers. + */ +function image_effect_integer_validate($element, &$form_state) { + $value = empty($element['#allow_negative']) ? $element['#value'] : preg_replace('/^-/', '', $element['#value']); + if ($element['#value'] != '' && !ctype_digit($value)) { + if (empty($element['#allow_negative'])) { + form_error($element, t('!name must be an integer.', array('!name' => $element['#title']))); + } + else { + form_error($element, t('!name must be a positive integer.', array('!name' => $element['#title']))); + } + } +} + +/** + * Element validate handler to ensure a hexadecimal color value. + */ +function image_effect_color_validate($element, &$form_state) { + if ($element['#value'] != '') { + $hex_value = preg_replace('/^#/', '', $element['#value']); + if (!preg_match('/^#/', $element['#value']) || strlen($hex_value) != 6 && strlen($hex_value) != 3 || !ctype_xdigit($hex_value)) { + form_error($element, t('!name must be a hexadecimal color value.', array('!name' => $element['#title']))); + } + } +} + +/** + * Element validate handler to ensure that either a height or a width is + * specified. + */ +function image_effect_scale_validate($element, &$form_state) { + if (empty($element['width']['#value']) && empty($element['height']['#value'])) { + form_error($element, t('Width and height can not both be blank.')); + } +} + +/** + * Form structure for the image resize form. + * + * Note that this is not a complete form, it only contains the portion of the + * form for configuring the resize options. Therefor it does not not need to + * include metadata about the effect, nor a submit button. + * + * @param $data + * The current configuration for this resize effect. + */ +function image_resize_form($data) { + $form['width'] = array( + '#type' => 'textfield', + '#title' => t('Width'), + '#default_value' => isset($data['width']) ? $data['width'] : '', + '#field_suffix' => ' ' . t('pixels'), + '#required' => TRUE, + '#size' => 10, + '#element_validate' => array('image_effect_integer_validate'), + '#allow_negative' => FALSE, + ); + $form['height'] = array( + '#type' => 'textfield', + '#title' => t('Height'), + '#default_value' => isset($data['height']) ? $data['height'] : '', + '#field_suffix' => ' ' . t('pixels'), + '#required' => TRUE, + '#size' => 10, + '#element_validate' => array('image_effect_integer_validate'), + '#allow_negative' => FALSE, + ); + return $form; +} + +/** + * Form structure for the image scale form. + * + * Note that this is not a complete form, it only contains the portion of the + * form for configuring the scale options. Therefor it does not not need to + * include metadata about the effect, nor a submit button. + * + * @param $data + * The current configuration for this scale effect. + */ +function image_scale_form($data) { + $form = image_resize_form($data); + $form['#element_validate'] = array('image_effect_scale_validate'); + $form['width']['#required'] = FALSE; + $form['height']['#required'] = FALSE; + $form['upscale'] = array( + '#type' => 'checkbox', + '#default_value' => (isset($data['upscale'])) ? $data['upscale'] : 0, + '#title' => t('Allow Upscaling'), + '#description' => t('Let scale make images larger than their original size'), + ); + return $form; +} + +/** + * Form structure for the image crop form. + * + * Note that this is not a complete form, it only contains the portion of the + * form for configuring the crop options. Therefor it does not not need to + * include metadata about the effect, nor a submit button. + * + * @param $data + * The current configuration for this crop effect. + */ +function image_crop_form($data) { + $data += array( + 'width' => '', + 'height' => '', + 'anchor' => 'center-center', + ); + + $form = image_resize_form($data); + $form['anchor'] = array( + '#type' => 'radios', + '#title' => t('Anchor'), + '#options' => array( + 'left-top' => t('Top') . ' ' . t('Left'), + 'center-top' => t('Top') . ' ' . t('Center'), + 'right-top' => t('Top') . ' ' . t('Right'), + 'left-center' => t('Center') . ' ' . t('Left'), + 'center-center' => t('Center'), + 'right-center' => t('Center') . ' ' . t('Right'), + 'left-bottom' => t('Bottom') . ' ' . t('Left'), + 'center-bottom' => t('Bottom') . ' ' . t('Center'), + 'right-bottom' => t('Bottom') . ' ' . t('Right'), + ), + '#theme' => 'image_anchor', + '#default_value' => $data['anchor'], + '#description' => t('The part of the image that will be retained during the crop.'), + ); + + return $form; +} + +/** + * Form structure for the image rotate form. + * + * Note that this is not a complete form, it only contains the portion of the + * form for configuring the rotate options. Therefor it does not not need to + * include metadata about the effect, nor a submit button. + * + * @param $data + * The current configuration for this rotate effect. + */ +function image_rotate_form($data) { + $form['degrees'] = array( + '#type' => 'textfield', + '#default_value' => (isset($data['degrees'])) ? $data['degrees'] : 0, + '#title' => t('Rotation angle'), + '#description' => t('The number of degrees the image should be rotated. Positive numbers are clockwise, negative are counter-clockwise.'), + '#field_suffix' => '°', + '#required' => TRUE, + '#size' => 6, + '#maxlength' => 4, + '#element_validate' => array('image_effect_integer_validate'), + '#allow_negative' => TRUE, + ); + $form['bgcolor'] = array( + '#type' => 'textfield', + '#default_value' => (isset($data['bgcolor'])) ? $data['bgcolor'] : '#FFFFFF', + '#title' => t('Background color'), + '#description' => t('The background color to use for exposed areas of the image. Use web-style hex colors (#FFFFFF for white, #000000 for black). Leave blank for transparency on image types that support it.'), + '#size' => 7, + '#maxlength' => 7, + '#element_validate' => array('image_effect_color_validate'), + ); + $form['random'] = array( + '#type' => 'checkbox', + '#default_value' => (isset($data['random'])) ? $data['random'] : 0, + '#title' => t('Randomize'), + '#description' => t('Randomize the rotation angle for each image. The angle specified above is used as a maximum.'), + ); + return $form; +} + +/** + * Display the page containing the list of image styles. + * + * @param $styles + * An array of all the image styles returned by image_get_styles(). + * @see image_get_styles() + * @ingroup themeable + */ +function theme_image_style_list($styles) { + $header = array(t('Style name'), array('data' => t('Operations'), 'colspan' => 3)); + $rows = array(); + foreach ($styles as $style) { + $row = array(); + $row[] = l($style['name'], 'admin/settings/image-styles/edit/' . $style['name']); + $link_attributes = array( + 'attributes' => array( + 'class' => 'image-style-link', + ), + ); + $row[] = l(t('edit'), 'admin/settings/image-styles/edit/' . $style['name'], $link_attributes); + $row[] = l(t('delete'), 'admin/settings/image-styles/delete/' . $style['name'], $link_attributes); + $rows[] = $row; + } + + if (empty($rows)) { + $rows[] = array(array( + 'colspan' => 4, + 'data' => t('There are currently no styles. Add a new one.', array('!url' => url('admin/settings/image-styles/add'))), + )); + } + + return theme('table', $header, $rows); +} + +/** + * Theme callback for listing the effects within a specific image style. + * + * @param $form + * An associative array containing the structure of the effects group. + * @ingroup themeable + */ +function theme_image_style_effects($form) { + $rows = array(); + + foreach (element_children($form) as $key) { + if (is_numeric($key)) { + $form[$key]['weight']['#attributes']['class'] = 'image-effect-order-weight'; + $summary = drupal_render($form[$key]['summary']); + $row = array(); + $row[] = drupal_render($form[$key]['label']) . (empty($summary) ? '' : ' ' . $summary); + $row[] = drupal_render($form[$key]['weight']); + $row[] = drupal_render($form[$key]['configure']); + $row[] = drupal_render($form[$key]['remove']); + } + else { + // Add the row for adding a new effect. + $form['new']['weight']['#attributes']['class'] = 'image-effect-order-weight'; + $row = array(); + $row[] = '
' . drupal_render($form['new']['new']) . drupal_render($form['new']['add']) . '
'; + $row[] = drupal_render($form['new']['weight']); + $row[] = array('data' => '', 'colspan' => 2); + } + + $rows[] = array( + 'data' => $row, + 'class' => 'draggable', + ); + } + + $header = array( + t('Effect'), + t('Weight'), + array('data' => t('Operations'), 'colspan' => 2), + ); + + if (count($rows) == 1) { + array_unshift($rows, array(array( + 'data' => t('There are currently no effects in this style. Add one by selecting an option below.'), + 'colspan' => 4, + ))); + } + + $output = theme('table', $header, $rows, array('id' => 'image-style-effects')); + drupal_add_tabledrag('image-style-effects', 'order', 'sibling', 'image-effect-order-weight'); + return $output; +} + +/** + * Theme callback for displaying a preview of a style. + * + * @param $style + * The image style array being previewed. + * @ingroup themeable + */ +function theme_image_style_preview($style) { + $sample_image = variable_get('image_style_preview_image', drupal_get_path('module', 'image') . '/sample.png'); + $sample_width = 160; + $sample_height = 160; + + // Set up original file information. + $original_path = $sample_image; + $original_image = image_get_info($original_path); + if ($original_image['width'] > $original_image['height']) { + $original_width = min($original_image['width'], $sample_width); + $original_height = round($original_width / $original_image['width'] * $original_image['height']); + } + else { + $original_height = min($original_image['height'], $sample_height); + $original_width = round($original_height / $original_image['height'] * $original_image['width']); + } + $original_attributes = array_intersect_key($original_image, array('width' => '', 'height' => '')); + $original_attributes['style'] = 'width: ' . $original_width . 'px; height: ' . $original_height . 'px;'; + + // Set up preview file information. + $preview_file = image_style_path($style['name'], $original_path); + $preview_path = file_create_path($preview_file); + if (!file_exists($preview_path) && image_style_create_derivative($style, $original_path, $preview_file)) { + $preview_path = file_create_path($preview_file); + } + $preview_image = image_get_info($preview_path); + if ($preview_image['width'] > $preview_image['height']) { + $preview_width = min($preview_image['width'], $sample_width); + $preview_height = round($preview_width / $preview_image['width'] * $preview_image['height']); + } + else { + $preview_height = min($preview_image['height'], $sample_height); + $preview_width = round($preview_height / $preview_image['height'] * $preview_image['width']); + } + $preview_attributes = array_intersect_key($preview_image, array('width' => '', 'height' => '')); + $preview_attributes['style'] = 'width: ' . $preview_width . 'px; height: ' . $preview_height . 'px;'; + + // Timestamps are added on output to prevent caching of preview images. + $output = ''; + $output .= '
'; + $output .= '
'; + $output .= t('original (!actual)', array('!actual' => l(t('view actual size'), $original_path))); + $output .= '
'; + $output .= '' . theme('image', $original_path . '?' . time(), t('Sample original image'), '', $original_attributes, FALSE) . ''; + $output .= '
' . $original_image['height'] . 'px
'; + $output .= '
' . $original_image['width'] . 'px
'; + $output .= '
'; // End preview-image. + $output .= '
'; // End preview-image-wrapper. + $output .= '
'; + $output .= check_plain($style['name']); + $output .= ' ' . t('(!actual)', array('!actual' => l(t('view actual size'), file_create_url($preview_file) . '?' . time()))); + $output .= '
'; + $output .= '' . theme('image', file_create_url($preview_file) . '?' . time(), t('Sample modified image'), '', $preview_attributes, FALSE) . ''; + $output .= '
' . $preview_image['height'] . 'px
'; + $output .= '
' . $preview_image['width'] . 'px
'; + $output .= '
'; // End preview-image. + $output .= '
'; // End preview-image-wrapper. + $output .= '
'; // End image-style-preview. + + return $output; +} + +/** + * Theme callback for displaying a grid of checkboxes. + * + * @param $element + * A Form API element containing radio buttons. + * @ingroup themeable + */ +function theme_image_anchor($element) { + $element_children = element_children($element); + $size = ceil(sqrt(count($element_children))); + + $rows = array(); + $row = array(); + foreach ($element_children as $n => $key) { + $element[$key]['#attributes']['title'] = $element[$key]['#title']; + unset($element[$key]['#title']); + $row[] = drupal_render($element[$key]); + if ($n % $size == $size - 1) { + $rows[] = $row; + $row = array(); + } + } + + return theme('table', array(), $rows, array('class' => 'image-anchor')); +} + +/** + * Theme callback for image resize effect summary output. + * + * @param $data + * The current configuration for this resize effect. + * @ingroup themeable + */ +function theme_image_resize_summary($data) { + if ($data['width'] && $data['height']) { + return $data['width'] . 'x' . $data['height']; + } + else { + return ($data['width']) ? t('width @width', array('@width' => $data['width'])) : t('height @width', array('@height' => $data['height'])); + } +} + +/** + * Theme callback for image scale effect summary output. + * + * @param $data + * The current configuration for this scale effect. + * @ingroup themeable + */ +function theme_image_scale_summary($data) { + return theme('image_resize_summary', $data) . ' ' . ($data['upscale'] ? '(' . t('upscaling allowed') . ')' : ''); +} + +/** + * Theme callback for image crop effect summary output. + * + * @param $data + * The current configuration for this crop effect. + * @ingroup themeable + */ +function theme_image_crop_summary($data) { + return theme('image_resize_summary', $data); +} + +/** + * Theme callback for image rotate effect summary output. + * + * @param $data + * The current configuration for this rotate effect. + * @ingroup themeable + */ +function theme_image_rotate_summary($data) { + return ($data['random']) ? t('random between -@degrees° and @degrees°', array('@degrees' => $data['degrees'])) : t('@degrees°', array('@degrees' => $data['degrees'])); +} diff --git modules/image/image.api.php modules/image/image.api.php index 4239b52..c251492 100644 --- modules/image/image.api.php +++ modules/image/image.api.php @@ -21,18 +21,24 @@ * An array of image effects. This array is keyed on the machine-readable * effect name. Each effect is defined as an associative array containing the * following items: - * - "name": The human-readable name of the effect. + * - "label": The human-readable name of the effect. * - "effect callback": The function to call to perform this effect. * - "help": (optional) A brief description of the effect that will be shown * when adding or configuring this effect. + * - "form callback": (optional) The name of a function that will return a + * $form array providing a configuration form for this effect. + * - "summary theme": (optional) The name of a theme function that will output + * a summary of this effect's configuration. */ function hook_image_effect_info() { $effects = array(); $effects['mymodule_resize'] = array( - 'name' => t('Resize'), + 'label' => t('Resize'), 'help' => t('Resize an image to an exact set of dimensions, ignoring aspect ratio.'), 'effect callback' => 'mymodule_resize_image', + 'form callback' => 'mymodule_resize_form', + 'summary theme' => 'mymodule_resize_summary', ); return $effects; diff --git modules/image/image.effects.inc modules/image/image.effects.inc index 817737c..759dd99 100644 --- modules/image/image.effects.inc +++ modules/image/image.effects.inc @@ -12,34 +12,44 @@ function image_image_effect_info() { $effects = array( 'image_resize' => array( - 'name' => t('Resize'), + 'label' => t('Resize'), 'help' => t('Resizing will make images an exact set of dimensions. This may cause images to be stretched or shrunk disproportionately.'), 'effect callback' => 'image_resize_effect', + 'form callback' => 'image_resize_form', + 'summary theme' => 'image_resize_summary', ), 'image_scale' => array( - 'name' => t('Scale'), + 'label' => t('Scale'), 'help' => t('Scaling will maintain the aspect-ratio of the original image. If only a single dimension is specified, the other dimension will be calculated.'), 'effect callback' => 'image_scale_effect', + 'form callback' => 'image_scale_form', + 'summary theme' => 'image_scale_summary', ), 'image_scale_and_crop' => array( - 'name' => t('Scale and Crop'), + 'label' => t('Scale and crop'), 'help' => t('Scale and crop will maintain the aspect-ratio of the original image, then crop the larger dimension. This is most useful for creating perfectly square thumbnails without stretching the image.'), 'effect callback' => 'image_scale_and_crop_effect', + 'form callback' => 'image_resize_form', + 'summary theme' => 'image_resize_summary', ), 'image_crop' => array( - 'name' => t('Crop'), + 'label' => t('Crop'), 'help' => t('Cropping will remove portions of an image to make it the specified dimensions.'), 'effect callback' => 'image_crop_effect', + 'form callback' => 'image_crop_form', + 'summary theme' => 'image_crop_summary', ), 'image_desaturate' => array( - 'name' => t('Desaturate'), + 'label' => t('Desaturate'), 'help' => t('Desaturate converts an image to grayscale.'), 'effect callback' => 'image_desaturate_effect', ), 'image_rotate' => array( - 'name' => t('Rotate'), + 'label' => t('Rotate'), 'help' => t('Rotating an image may cause the dimensions of an image to increase to fit the diagonal.'), 'effect callback' => 'image_rotate_effect', + 'form callback' => 'image_rotate_form', + 'summary theme' => 'image_rotate_summary', ), ); diff --git modules/image/image.info modules/image/image.info index 7ff881c..4a7d5c5 100644 --- modules/image/image.info +++ modules/image/image.info @@ -5,6 +5,7 @@ package = Core version = VERSION core = 7.x files[] = image.module +files[] = image.admin.inc files[] = image.effects.inc files[] = image.install files[] = image.test diff --git modules/image/image.module modules/image/image.module index c185d8f..a610e7a 100644 --- modules/image/image.module +++ modules/image/image.module @@ -7,6 +7,33 @@ */ /** + * Implement of hook_help(). + */ +function image_help($path, $arg) { + switch ($path) { + case 'admin/help#image': + $naming_approaches = array(); + $naming_approaches[] = t('Based on where it will be used: !name', array('!name' => 'profile-picture')); + $naming_approaches[] = t('Describing its appearance: !name', array('!name' => 'square-85x85')); + $output = ''; + $output .= '

' . t('The Image module provides functionality for displaying images on your site though image styles.', array('!url' => url('admin/settings/image-styles'))) .'

'; + $output .= '

' . t('Image styles') . '

'; + $output .= '

' . t('Image styles allow your site to output an image in several different ways without affecting the original image. Image styles may be updated after they have been created and all images using that style will be automatically updated.') .'

'; + $output .= '

' . t('Every image style must have a name, which will be used in the URL of generated images. There are two common approaches to naming image styles:') . '

'; + $output .= theme('item_list', $naming_approaches); + $output .= '

' . t('Both approaches are common and which you choose depends on how you use the image style.') . '

'; + $output .= '

' . t('After creating an image style, effects may be added to the style. Image module comes with some basic effects such as crop, scale, desaturate, and rotate. Besides the ones included with Image, other modules may provide additional effects. Multiple effects may be combined together, such as using the crop and scale effect and the desaturate effect, you could create square, grayscale thumbnails.'); + return $output; + case 'admin/settings/image-styles': + return '

' . t('Image styles commonly provide thumbnail sizes by scaling and cropping images, but can also add various effects before an image is displayed. When an image is displayed with a style, a new file is created and the original image is left unchanged.') . '

'; + case 'admin/settings/image-styles/edit/%/add/%': + case 'admin/settings/image-styles/edit/%/effects/%': + $effect = ($arg[5] == 'add') ? image_effect_definition_load($arg[6]) : image_effect_load($arg[6]); + return isset($effect['help']) ? ('

' . $effect['help'] . '

') : NULL; + } +} + +/** * Implement hook_menu(). */ function image_menu() { @@ -19,6 +46,79 @@ function image_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + $items['admin/settings/image-styles'] = array( + 'title' => 'Image styles', + 'description' => 'Configure styles that can be used for resizing or adjusting images on display.', + 'page callback' => 'image_style_list', + 'access arguments' => array('administer image styles'), + ); + $items['admin/settings/image-styles/list'] = array( + 'title' => 'List', + 'description' => 'List the current image styles on the site.', + 'page callback' => 'image_style_list', + 'access arguments' => array('administer image styles'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => 1, + ); + $items['admin/settings/image-styles/add'] = array( + 'title' => 'Add style', + 'description' => 'Add a new image style.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_style_add_form'), + 'access arguments' => array('administer image styles'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 2, + ); + $items['admin/settings/image-styles/edit/%image_style'] = array( + 'title' => 'Edit style', + 'title callback' => 'image_style_title', + 'title arguments' => array('!name', 4), + 'description' => 'Configure an image style.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_style_form', 4), + 'access arguments' => array('administer image styles'), + 'type' => MENU_CALLBACK, + ); + $items['admin/settings/image-styles/delete/%image_style'] = array( + 'title' => 'Delete style', + 'title callback' => 'image_style_title', + 'title arguments' => array('Delete !name', 4), + 'description' => 'Delete an image style.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_style_delete_form', 4, TRUE), + 'access arguments' => array('administer image styles'), + 'type' => MENU_CALLBACK, + ); + $items['admin/settings/image-styles/edit/%image_style/effects/%image_effect'] = array( + 'title' => 'Edit image effect', + 'title callback' => 'image_effect_title', + 'title arguments' => array('!label effect', 6), + 'description' => 'Edit an exiting effect within a style.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_effect_form', 4, 6), + 'access arguments' => array('administer image styles'), + 'type' => MENU_CALLBACK, + ); + $items['admin/settings/image-styles/edit/%image_style/effects/%image_effect/delete'] = array( + 'title' => 'Delete image effect', + 'title callback' => 'image_effect_title', + 'title arguments' => array('Delete !label', 6), + 'description' => 'Delete an exiting effect from a style.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_effect_delete_form', 4, 6), + 'access arguments' => array('administer image styles'), + 'type' => MENU_CALLBACK, + ); + $items['admin/settings/image-styles/edit/%image_style/add/%image_effect_definition'] = array( + 'title' => 'Add image effect', + 'title callback' => 'image_effect_title', + 'title arguments' => array('Add !label effect', 6), + 'description' => 'Add a new effect to a style.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_effect_form', 4, 6), + 'access arguments' => array('administer image styles'), + 'type' => MENU_CALLBACK, + ); return $items; } @@ -38,6 +138,42 @@ function image_theme() { 'getsize' => TRUE, ), ), + 'image_style_list' => array( + 'arguments' => array('styles' => NULL), + ), + 'image_style_effects' => array( + 'arguments' => array('form' => NULL), + ), + 'image_style_preview' => array( + 'arguments' => array('style' => NULL), + ), + 'image_anchor' => array( + 'arguments' => array('element' => NULL), + ), + 'image_resize_summary' => array( + 'arguments' => array('data' => NULL), + ), + 'image_scale_summary' => array( + 'arguments' => array('data' => NULL), + ), + 'image_crop_summary' => array( + 'arguments' => array('data' => NULL), + ), + 'image_rotate_summary' => array( + 'arguments' => array('data' => NULL), + ), + ); +} + +/** + * Implement hook_permission(). + */ +function image_permission() { + return array( + 'administer image styles' => array( + 'title' => t('Administer image styles'), + 'description' => t('Create and modify styles for generating image modifications such as thumbnails.'), + ), ); } diff --git modules/image/image.test modules/image/image.test index 313f5b1..6057f8f 100644 --- modules/image/image.test +++ modules/image/image.test @@ -201,3 +201,200 @@ class ImageEffectsUnitTest extends ImageToolkitTestCase { $this->assertEqual($calls['rotate'][0][2], 0xffffff, t('Background color was passed correctly')); } } + +/** + * Tests creation, deletion, and editing of image styles and effects. + */ +class ImageAdminStylesUnitTest extends DrupalWebTestCase { + + function getInfo() { + return array( + 'name' => t('Image styles and effects UI configuration'), + 'description' => t('Tests creation, deletion, and editing of image styles and effects at the UI level.'), + 'group' => t('Image') + ); + } + + /** + * Implementation of setUp(). + */ + function setUp() { + parent::setUp(); + + // Create an administrative user. + $this->admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer image styles')); + $this->drupalLogin($this->admin_user); + } + + /** + * Given an image style, generate an image. + */ + function createSampleImage($style) { + static $file_path; + + // First, we need to make sure we have an image in our testing + // file directory. Copy over an image on the first run. + if (!isset($file_path)) { + $file = reset($this->drupalGetTestFiles('image')); + $file_path = file_unmanaged_copy($file->filename); + } + + return image_style_url($style['name'], $file_path) ? $file_path : FALSE; + } + + /** + * Count the number of images currently create for a style. + */ + function getImageCount($style) { + $directory = file_directory_path() . '/styles/' . $style['name']; + return count(file_scan_directory($directory, '/.*/')); + } + + function testStyle() { + // Setup a style to be created and effects to add to it. + $style_name = strtolower($this->randomName(10)); + $style_path = 'admin/settings/image-styles/edit/' . $style_name; + $effect_edits = array( + 'image_resize' => array( + 'data[width]' => 100, + 'data[height]' => 101, + ), + 'image_scale' => array( + 'data[width]' => 110, + 'data[height]' => 111, + 'data[upscale]' => 1, + ), + 'image_scale_and_crop' => array( + 'data[width]' => 120, + 'data[height]' => 121, + ), + 'image_crop' => array( + 'data[width]' => 130, + 'data[height]' => 131, + 'data[anchor]' => 'center-center', + ), + 'image_desaturate' => array( + // No options for desaturate. + ), + 'image_rotate' => array( + 'data[degrees]' => 5, + 'data[random]' => 1, + 'data[bgcolor]' => '#FFFF00', + ), + ); + + /* Add style form. */ + + $edit = array( + 'name' => $style_name, + ); + $this->drupalPost('admin/settings/image-styles/add', $edit, t('Create new style')); + $this->assertRaw(t('Style %name was created.', array('%name' => $style_name)), t('Image style successfully created.')); + + /* Add effect form. */ + + // Add each sample effect to the style. + foreach ($effect_edits as $effect => $edit) { + // Add the effect. + $this->drupalPost($style_path, array('new' => $effect), t('Add')); + if (!empty($edit)) { + $this->drupalPost(NULL, $edit, t('Add effect')); + } + } + + /* Edit effect form. */ + + // Revisit each form to make sure the effect was saved. + $style = image_style_load($style_name); + + foreach ($style['effects'] as $ieid => $effect) { + $this->drupalGet($style_path . '/effects/' . $ieid); + foreach ($effect_edits[$effect['name']] as $field => $value) { + $this->assertFieldByName($field, $value, t('The %field field in the %effect effect has the correct value of %value.', array('%field' => $field, '%effect' => $effect['name'], '%value' => $value))); + } + } + + /* Image style overview form (ordering and renaming). */ + + // Confirm the order of effects is maintained according to the order we + // added the fields. + $effect_edits_order = array_keys($effect_edits); + $effects_order = array_values($style['effects']); + $order_correct = TRUE; + foreach ($effects_order as $index => $effect) { + if ($effect_edits_order[$index] != $effect['name']) { + $order_correct = FALSE; + } + } + $this->assertTrue($order_correct, t('The order of the effects is correctly set by default.')); + + // Test the style overview form. + // Change the name of the style and adjust the weights of effects. + $style_name = strtolower($this->randomName(10)); + $weight = count($effect_edits); + $edit = array( + 'name' => $style_name, + ); + foreach ($style['effects'] as $ieid => $effect) { + $edit['effects[' . $ieid . '][weight]'] = $weight; + $weight--; + } + + // Create an image to make sure it gets flushed after saving. + $image_path = $this->createSampleImage($style); + $this->assertEqual($this->getImageCount($style), 1, t('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path))); + + $this->drupalPost($style_path, $edit, t('Update style')); + + // Note that after changing the style name, the style path is changed. + $style_path = 'admin/settings/image-styles/edit/' . $style_name; + + // Check that the URL was updated. + $this->drupalGet($style_path); + $this->assertResponse(200, t('Image style %original renamed to %new', array('%original' => $style['name'], '%new' => $style_name))); + + // Check that the image was flushed after updating the style. + // This is especially important when renaming the style. Make sure that + // the old image directory has been deleted. + $this->assertEqual($this->getImageCount($style), 0, t('Image style %style was flushed after renaming the style and updating the order of effects.', array('%style' => $style['name']))); + + // Load the style by the new name with the new weights. + drupal_static_reset('image_styles'); + $style = image_style_load($style_name, NULL); + + // Confirm the new style order was saved. + $effect_edits_order = array_reverse($effect_edits_order); + $effects_order = array_values($style['effects']); + $order_correct = TRUE; + foreach ($effects_order as $index => $effect) { + if ($effect_edits_order[$index] != $effect['name']) { + $order_correct = FALSE; + } + } + $this->assertTrue($order_correct, t('The order of the effects is correctly set by default.')); + + /* Image effect deletion form. */ + + // Create an image to make sure it gets flushed after deleting an effect. + $image_path = $this->createSampleImage($style); + $this->assertEqual($this->getImageCount($style), 1, t('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path))); + + // Test effect deletion form. + $effect = array_pop($style['effects']); + $this->drupalPost($style_path . '/effects/' . $effect['ieid'] . '/delete', array(), t('Delete')); + $this->assertRaw(t('The image effect %name has been deleted.', array('%name' => $effect['label'])), t('Image effect deleted.')); + + /* Style deletion form. */ + + // Delete the style. + $this->drupalPost('admin/settings/image-styles/delete/' . $style_name, array(), t('Delete')); + + // Confirm the style directory has been removed. + $directory = file_directory_path() . '/styles/' . $style_name; + $this->assertFalse(is_dir($directory), t('Image style %style directory removed on style deletion.', array('%style' => $style['name']))); + + drupal_static_reset('image_styles'); + $this->assertFalse(image_style_load($style_name), t('Image style %style successfully deleted.', array('%style' => $style['name']))); + + } +} diff --git modules/user/user.admin.inc modules/user/user.admin.inc index 349b99a..e929ab8 100644 --- modules/user/user.admin.inc +++ modules/user/user.admin.inc @@ -363,26 +363,29 @@ function user_admin_settings() { if (module_exists('image')) { $form['personalization']['pictures']['settings']['user_picture_style'] = array( '#type' => 'select', - '#title' => t('Picture style'), + '#title' => t('Picture display style'), '#options' => image_style_options(TRUE), '#default_value' => variable_get('user_picture_style', ''), + '#description' => t('The style selected will be used on display, while the original image is retained. Styles may be configured in the Image styles administration area.', array('!url' => url('admin/settings/image-styles'))), ); } $form['personalization']['pictures']['user_picture_dimensions'] = array( '#type' => 'textfield', - '#title' => t('Picture maximum dimensions'), + '#title' => t('Picture upload dimensions'), '#default_value' => variable_get('user_picture_dimensions', '85x85'), - '#size' => 15, + '#size' => 10, '#maxlength' => 10, - '#description' => t('Maximum dimensions for pictures, in pixels.'), + '#field_suffix' => ' ' . t('pixels'), + '#description' => t('Maximum allowed dimensions for uploaded pictures.'), ); $form['personalization']['pictures']['user_picture_file_size'] = array( '#type' => 'textfield', - '#title' => t('Picture maximum file size'), + '#title' => t('Picture upload file size'), '#default_value' => variable_get('user_picture_file_size', '30'), - '#size' => 15, + '#size' => 10, '#maxlength' => 10, - '#description' => t('Maximum file size for pictures, in kB.'), + '#field_suffix' => ' ' . t('KB'), + '#description' => t('Maximum allowed file size for uploaded pictures.'), ); $form['personalization']['pictures']['user_picture_guidelines'] = array( '#type' => 'textarea',