diff options
author | Dries Buytaert <dries@buytaert.net> | 2009-07-12 08:36:35 +0000 |
---|---|---|
committer | Dries Buytaert <dries@buytaert.net> | 2009-07-12 08:36:35 +0000 |
commit | 6dc5b5449d6c52b433e6b712786c2e49778a363f (patch) | |
tree | 66e4926fa3dcc0a33f46f4145ca5b72bf7458943 /modules/image/image.module | |
parent | 065fa60551edddc27a30d04cecb58b30b7ac8e3c (diff) | |
download | brdo-6dc5b5449d6c52b433e6b712786c2e49778a363f.tar.gz brdo-6dc5b5449d6c52b433e6b712786c2e49778a363f.tar.bz2 |
- Patch #491456 by quicksketch, drewish, et al: image effects and actions.
Diffstat (limited to 'modules/image/image.module')
-rw-r--r-- | modules/image/image.module | 720 |
1 files changed, 720 insertions, 0 deletions
diff --git a/modules/image/image.module b/modules/image/image.module new file mode 100644 index 000000000..a979e6e5f --- /dev/null +++ b/modules/image/image.module @@ -0,0 +1,720 @@ +<?php +// $Id$ + +/** + * @file + * Exposes global functionality for creating image styles. + */ + +/** + * Implement hook_menu(). + */ +function image_menu() { + $items = array(); + + $items['image/generate/%image_style'] = array( + 'title' => 'Generate image style', + 'page callback' => 'image_style_generate', + 'page arguments' => array(2), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Implement hook_theme(). + */ +function image_theme() { + return array( + 'image_style' => array( + 'arguments' => array('style' => NULL, 'path' => NULL, 'alt' => '', 'title' => '', 'attributes' => NULL, 'getsize' => TRUE), + ), + ); +} + +/** + * Implement hook_flush_caches(). + */ +function image_flush_caches() { + return array('cache_image'); +} + +/** + * Implement hook_file_download(). + * + * Control the access to files underneath the styles directory. + */ +function image_file_download($filepath) { + if (strpos($filepath, 'styles/') === 0) { + $args = explode('/', $filepath); + // Discard the first part of the path (styles). + array_shift($args); + // Get the style name from the second part. + $style_name = array_shift($args); + // Then the remaining parts are the path to the image. + $original_path = implode('/', $args); + + // Check that the file exists and is an image. + if ($info = image_get_info(file_create_path($filepath))) { + // Check the permissions of the original to grant access to this image. + $headers = module_invoke_all('file_download', $original_path); + if (!in_array(-1, $headers)) { + return array( + 'Content-Type: ' . $info['mime_type'], + 'Content-Length: ' . $info['file_size'], + ); + } + } + return -1; + } +} + +/** + * Implement hook_file_move(). + */ +function image_file_move($file, $source) { + // Delete any image derivatives at the original image path. + image_path_flush($file->filepath); +} + +/** + * Implement hook_file_delete(). + */ +function image_file_delete($file) { + // Delete any image derivatives of this image. + image_path_flush($file->filepath); +} + +/** + * Clear cached versions of a specific file in all styles. + * + * @param $path + * The Drupal file path to the original image. + */ +function image_path_flush($path) { + $path = file_directory_strip($path); + $styles = image_styles(); + foreach ($styles as $style) { + if ($path = file_create_path('styles/' . $style['name'] . '/' . $path)) { + file_unmanaged_delete($path); + } + } +} + +/** + * Get an array of all styles and their settings. + * + * @return + * An array of styles keyed by the image style ID (isid). + * @see image_style_load() + */ +function image_styles() { + $styles = &drupal_static(__FUNCTION__); + + // Grab from cache or build the array. + if (!isset($styles)) { + if ($cache = cache_get('image_styles', 'cache')) { + $styles = $cache->data; + } + else { + $styles = array(); + $result = db_select('image_styles', NULL, array('fetch' => PDO::FETCH_ASSOC)) + ->fields('image_styles') + ->orderBy('name') + ->execute(); + foreach ($result as $style) { + $styles[$style['name']] = $style; + $styles[$style['name']]['effects'] = image_style_effects($style); + } + + cache_set('image_styles', $styles); + } + } + + return $styles; +} + +/** + * Load a style by style name or ID. May be used as a loader for menu items. + * + * @param $name + * The name of the style. + * @param $isid + * Optional. The numeric id of a style if the name is not known. + * @return + * An image style array containing the following keys: + * - "isid": The unique image style ID. + * - "name": The unique image style name. + * - "effects": An array of effects within this style. + * If the style name or ID is not valid, an empty array is returned. + * @see image_effect_load() + */ +function image_style_load($name = NULL, $isid = NULL) { + $styles = image_styles(); + + // If retrieving by name. + if (isset($name) && isset($styles[$name])) { + return $styles[$name]; + } + + // If retrieving by image style id. + if (isset($isid)) { + foreach ($styles as $name => $style) { + if ($style['isid'] == $isid) { + return $style; + } + } + } + + // Otherwise the style was not found. + return FALSE; +} + +/** + * Save an image style. + * + * @param style + * An image style array. + * @return + * A style array. In the case of a new style, 'isid' will be populated. + */ +function image_style_save($style) { + if (isset($style['isid']) && is_numeric($style['isid'])) { + // Load the existing style to make sure we account for renamed styles. + $old_style = image_style_load(NULL, $style['isid']); + image_style_flush($old_style); + drupal_write_record('image_styles', $style, 'isid'); + if ($old_style['name'] != $style['name']) { + $style['old_name'] = $old_style['name']; + } + } + else { + drupal_write_record('image_styles', $style); + $style['is_new'] = TRUE; + } + + // Let other modules update as necessary on save. + module_invoke_all('image_style_save', $style); + + // Clear all caches and flush. + image_style_flush($style); + + return $style; +} + +/** + * Delete an image style. + * + * @param $style + * An image style array. + * @param $replacement_style_name + * (optional) When deleting a style, specify a replacement style name so + * that existing settings (if any) may be converted to a new style. + * @return + * TRUE on success. + */ +function image_style_delete($style, $replacement_style_name = '') { + image_style_flush($style); + + db_delete('image_effects')->condition('isid', $style['isid'])->execute(); + db_delete('image_styles')->condition('isid', $style['isid'])->execute(); + + // Let other modules update as necessary on save. + $style['old_name'] = $style['name']; + $style['name'] = $replacement_style_name; + module_invoke_all('image_style_delete', $style); + + return TRUE; +} + +/** + * Load all the effects for an image style. + * + * @param $style + * An image style array. + * @return + * An array of effects associated with specified style in the format + * array('isid' => array()), or an empty array if the specified style has + * no effects. + */ +function image_style_effects($style) { + $effects = image_effects(); + $style_effects = array(); + foreach ($effects as $effect) { + if ($style['isid'] == $effect['isid']) { + $style_effects[$effect['ieid']] = $effect; + } + } + + return $style_effects; +} + +/** + * Get an array of image styles suitable for using as select list options. + * + * @param $include_empty + * If TRUE a <none> option will be inserted in the options array. + * @return + * Array of image styles both key and value are set to style name. + */ +function image_style_options($include_empty = TRUE) { + $styles = image_styles(); + $options = array(); + if ($include_empty && !empty($styles)) { + $options[''] = t('<none>'); + } + $options = array_merge($options, drupal_map_assoc(array_keys($styles))); + if (empty($options)) { + $options[''] = t('No defined styles'); + } + return $options; +} + +/** + * Menu callback; Given a style and image path, generate a derivative. + * + * This menu callback is always served after checking a token to prevent + * generation of unnecessary images. After generating an image transfer it to + * the requesting agent via file_transfer(). + */ +function image_style_generate() { + $args = func_get_args(); + $style = array_shift($args); + $style_name = $style['name']; + $path = implode('/', $args); + + $source = file_create_path($path); + $path_md5 = md5($path); + $destination = image_style_path($style['name'], $path); + + // Check that it's a defined style and that access was granted by + // image_style_generate_url(). + if (!$style || !cache_get('access:' . $style_name . ':' . $path_md5, 'cache_image')) { + drupal_access_denied(); + exit(); + } + + // Don't start generating the image if it is already in progress. + $cid = 'generate:' . $style_name . ':' . $path_md5; + if (cache_get($cid, 'cache_image')) { + print t('Image generation in progress, please try again shortly.'); + exit(); + } + + // If the image has already been generated then send it. + if ($image = image_load($destination)) { + file_transfer($image->source, array('Content-type: ' . $image->info['mime_type'], 'Content-length: ' . $image->info['file_size'])); + } + + // Set a cache entry designating this image as being in-process. + cache_set($cid, $destination, 'cache_image'); + + // Try to generate the image. + if (image_style_create_derivative($style, $source, $destination)) { + $image = image_load($destination); + cache_clear_all($cid, 'cache_image'); + file_transfer($image->source, array('Content-type: ' . $image->info['mime_type'], 'Content-length: ' . $image->info['file_size'])); + } + else { + cache_clear_all($cid, 'cache_image'); + watchdog('image', 'Unable to generate the derived image located at %path.', $destination); + print t('Error generating image.'); + exit(); + } +} + +/** + * Create a new image based on an image style. + * + * @param $style + * An image style array. + * @param $source + * Path of the source file. + * @param $destination + * Path of the destination file. + * @return + * TRUE if an image derivative is generated, FALSE if no image derivative + * is generated. NULL if the derivative is being generated. + */ +function image_style_create_derivative($style, $source, $destination) { + // Get the folder for the final location of this style. + $directory = dirname($destination); + + // Build the destination folder tree if it doesn't already exist. + if (!file_check_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) { + watchdog('image', 'Failed to create style directory: %directory', array('%directory' => $directory), WATCHDOG_ERROR); + return FALSE; + } + + if (!$image = image_load($source)) { + return FALSE; + } + + foreach ($style['effects'] as $effect) { + image_effect_apply($image, $effect); + } + + if (!image_save($image, $destination)) { + if (file_exists($destination)) { + watchdog('image', 'Cached image file %destination already exists. There may be an issue with your rewrite configuration.', array('%destination' => $destination), WATCHDOG_ERROR); + } + return FALSE; + } + + return TRUE; +} + +/** + * Flush cached media for a style. + * + * @param $style + * An image style array. + */ +function image_style_flush($style) { + $style_directory = realpath(file_directory_path() . '/styles/' . $style['name']); + if (is_dir($style_directory)) { + file_unmanaged_delete_recursive($style_directory); + } + + // Let other modules update as necessary on flush. + module_invoke_all('image_style_flush', $style); + + // Clear image style and effect caches. + cache_clear_all('image_styles', 'cache'); + cache_clear_all('image_effects', 'cache'); + drupal_static_reset('image_styles'); + drupal_static_reset('image_effects'); + + // Clear page caches when flushing. + if (module_exists('block')) { + cache_clear_all('*', 'cache_block', TRUE); + } + cache_clear_all('*', 'cache_page', TRUE); +} + +/** + * Return the complete URL to an image when using a style. + * + * If the image has already been created then its location will be returned. If + * it does not then image_style_generate_url() will be called. + * + * @param $style_name + * The name of the style to be used with this image. + * @param $path + * The path to the image. + * @return + * The absolute URL where a style image can be downloaded, suitable for use + * in an <img> tag. If the site is using the default method for generating + * images, the image may not yet exist and will only be created when a + * visitor's browser requests the file. + * @see image_style_generate_url() + * @see image_style_path() + */ +function image_style_url($style_name, $path) { + $style_path = image_style_path($style_name, $path); + if (file_exists($style_path)) { + return file_create_url($style_path); + } + return image_style_generate_url($style_name, $path); +} + +/** + * Return the URL for an image derivative given a style and image path. + * + * This function is the default image generation method. It returns a URL for + * an image that can be used in an <img> tag. When the browser requests the + * image at image/generate/[style_name]/[path] the image is generated if it does + * not already exist and then served to the browser. This allows each image to + * have its own PHP instance (and memory limit) for generation of the new image. + * + * @param $style_name + * The name of the style to be used with this image. + * @param $path + * The path to the image. + * @return + * The absolute URL where a style image can be downloaded, suitable for use + * in an <img> tag. Requesting the URL will cause the image to be created. + * @see image_style_generate() + * @see image_style_url() + */ +function image_style_generate_url($style_name, $path) { + $destination = image_style_path($style_name, $path); + + // If the image already exists use that rather than regenerating it. + if (file_exists($destination)) { + return image_style_url($style_name, $path); + } + + // Disable page cache for this request. This prevents anonymous users from + // needlessly hitting the image generation URL when the image already exists. + $GLOBALS['conf']['cache'] = CACHE_DISABLED; + + // Set a cache entry to grant access to this style/image path. This will be + // checked by image_style_generate(). + cache_set('access:' . $style_name . ':' . md5($path), 1, 'cache_image', time() + 600); + + // Generate a callback path for the image. + $url = url('image/generate/' . $style_name . '/' . $path, array('absolute' => TRUE)); + return $url; +} + +/** + * Return a relative path to an image when using a style. + * + * The path returned by this function may not exist. The default generation + * method only creates images when they are requested by a user's browser. + * + * @param $style_name + * The name of the style to be used with this image. + * @param $path + * The path to the image. + * @return + * The path to an image style image relative to Drupal's root. + * @see image_style_url() + */ +function image_style_path($style_name, $path) { + return file_directory_path() . '/styles/' . $style_name . '/' . file_directory_strip($path); +} + +/** + * Pull in effects exposed by other modules using hook_image_effect_info(). + * + * @return + * An array of effects to be used when transforming images. + * @see hook_image_effect_info() + * @see image_effect_definition_load() + */ +function image_effect_definitions() { + $effects = &drupal_static(__FUNCTION__); + + if (!isset($effects)) { + if ($cache = cache_get('image_effects') && !empty($cache->data)) { + $effects = $cache->data; + } + else { + $effects = array(); + foreach (module_implements('image_effect_info') as $module) { + foreach (module_invoke($module, 'image_effect_info') as $name => $effect) { + // Ensure the current toolkit supports the effect. + $effect['module'] = $module; + $effect['name'] = $name; + $effect['data'] = isset($effect['data']) ? $effect['data'] : array(); + $effects[$name] = $effect; + }; + } + uasort($effects, '_image_effect_definitions_sort'); + cache_set('image_effects', $effects); + } + } + + return $effects; +} + +/** + * Load the definition for an effect. + * + * The effect definition is a set of core properties for an effect, not + * containing any user-settings. The definition defines various functions to + * call when configuring or executing an effect. This loader is mostly for + * internal use within image.module. Use image_effect_load() or + * image_style_load() to get effects that contain configuration. + * + * @param $effect + * The name of the effect definition to load. + * @return + * An array containing the image effect definition with the following keys: + * - "effect": The unique name for the effect being performed. Usually prefixed + * with the name of the module providing the effect. + * - "module": The module providing the effect. + * - "help": A description of the effect. + * - "function": The name of the function that will execute the effect. + * - "form": i'm (optional) The name of a function to configure the effect. + * - "summary": (optional) The name of a theme function that will display a + * one-line summary of the effect. Does not include the "theme_" prefix. + */ +function image_effect_definition_load($effect) { + $definitions = image_effect_definitions(); + return isset($definitions[$effect]) ? $definitions[$effect] : FALSE; +} + +/** + * Load all image effects from the database. + * + * @return + * An array of all image effects. + * @see image_effect_load() + */ +function image_effects() { + $effects = &drupal_static(__FUNCTION__); + + if (!isset($effects)) { + $effects = array(); + + // Add database image effects. + $result = db_select('image_effects', NULL, array('fetch' => PDO::FETCH_ASSOC)) + ->fields('image_effects') + ->orderBy('image_effects.weight', 'ASC') + ->execute(); + foreach ($result as $effect) { + $effect['data'] = unserialize($effect['data']); + $definition = image_effect_definition_load($effect['name']); + // Do not load effects whose definition cannot be found. + if ($definition) { + $effect = array_merge($definition, $effect); + $effects[$effect['ieid']] = $effect; + } + } + } + + return $effects; +} + +/** + * Load a single image effect. + * + * @param $ieid + * The image effect ID. + * @return + * An image effect array, consisting of the following keys: + * - "ieid": The unique image effect ID. + * - "isid": The unique image style ID that contains this effect. + * - "weight": The weight of this effect within the image style. + * - "effect": The name of the effect definition that powers this effect. + * - "data": An associative array of configuration options for this effect. + * Besides these keys, the entirety of the image definition is merged into + * the image effect array. Returns FALSE if the specified effect cannot be + * found. + * @see image_style_load() + * @see image_effect_definition_load() + */ +function image_effect_load($ieid) { + $effects = image_effects(); + return isset($effects[$ieid]) ? $effects[$ieid] : FALSE; +} + +/** + * Save an image effect. + * + * @param $effect + * An image effect array. + * @return + * An image effect array. In the case of a new effect 'ieid' will be set. + */ +function image_effect_save($effect) { + if (!empty($effect['ieid'])) { + drupal_write_record('image_effects', $effect, 'ieid'); + } + else { + drupal_write_record('image_effects', $effect); + } + $style = image_style_load(NULL, $effect['isid']); + image_style_flush($style); + return $effect; +} + +/** + * Delete an image effect. + * + * @param $effect + * An image effect array. + */ +function image_effect_delete($effect) { + db_delete('image_effects')->condition('ieid', $effect['ieid'])->execute(); + $style = image_style_load(NULL, $effect['isid']); + image_style_flush($style); +} + +/** + * Given an image object and effect, perform the effect on the file. + * + * @param $image + * An image object returned by image_load(). + * @param $effect + * An image effect array. + * @return + * TRUE on success. FALSE if unable to perform effect on image. + */ +function image_effect_apply(&$image, $effect) { + if (drupal_function_exists($effect['effect callback'])) { + return call_user_func($effect['effect callback'], $image, $effect['data']); + } + return FALSE; +} + +/** + * Return a themed image using a specific image style. + * + * @param $style_name + * The name of the style to be used to alter the original image. + * @param $path + * The path of the image file relative to the Drupal files directory. + * This function does not work with images outside the files directory nor + * with remotely hosted images. + * @param $alt + * The alternative text for text-based browsers. + * @param $title + * The title text is displayed when the image is hovered in some popular + * browsers. + * @param $attributes + * Associative array of attributes to be placed in the img tag. + * @param $getsize + * If set to TRUE, the image's dimension are fetched and added as + * width/height attributes. + * @return + * A string containing the image tag. + * @ingroup themeable + */ +function theme_image_style($style_name, $path, $alt = '', $title = '', $attributes = NULL, $getsize = TRUE) { + // theme_image() can only honor the $getsize parameter with local file paths. + // The derivative image is not created until it has been requested so the file + // may not yet exist, in this case we just fallback to the URL. + $style_path = image_style_path($style_name, $path); + if (!file_exists($style_path)) { + $style_path = image_style_url($style_name, $path); + } + return theme('image', $style_path, $alt, $title, $attributes, $getsize); +} + +/** + * Accept a percentage and return it in pixels. + */ +function image_filter_percent($value, $current_pixels) { + if (strpos($value, '%') !== FALSE) { + $value = str_replace('%', '', $value) * 0.01 * $current_pixels; + } + return $value; +} + +/** + * Accept a keyword (center, top, left, etc) and return it as a pixel offset. + * + * @param $value + * @param $current_pixels + * @param $new_pixels + */ +function image_filter_keyword($value, $current_pixels, $new_pixels) { + switch ($value) { + case 'top': + case 'left': + return 0; + + case 'bottom': + case 'right': + return $current_pixels - $new_pixels; + + case 'center': + return $current_pixels / 2 - $new_pixels / 2; + } + return $value; +} + +/** + * Internal function for sorting image effect definitions through uasort(). + * + * @see image_effect_definitions() + */ +function _image_effect_definitions_sort($a, $b) { + return strcasecmp($a['name'], $b['name']); +} |