summaryrefslogtreecommitdiff
path: root/modules/image/image.module
diff options
context:
space:
mode:
authorDries Buytaert <dries@buytaert.net>2009-07-12 08:36:35 +0000
committerDries Buytaert <dries@buytaert.net>2009-07-12 08:36:35 +0000
commit6dc5b5449d6c52b433e6b712786c2e49778a363f (patch)
tree66e4926fa3dcc0a33f46f4145ca5b72bf7458943 /modules/image/image.module
parent065fa60551edddc27a30d04cecb58b30b7ac8e3c (diff)
downloadbrdo-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.module720
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']);
+}