/path
*
* @todo: improve image resizing, reduce distortion.
* @todo: add watermarking capabilities.
* @todo: split action/handlers into their own little .inc files.
* @todo: enforce permissions.
*
* Notes:
* To add a new handler,
* add fields and select option to _imagecache_actions_form;
* add handling code to imagecache_cache
*
*/
/*********************************************************************************************
* Drupal Hooks
*********************************************************************************************/
/**
* Implementation of hook_perm().
*/
function imagecache_perm() {
$perms = array('administer imagecache', 'flush imagecache');
foreach(imagecache_presets() as $preset) {
$perms[] = 'view imagecache '. $preset['presetname'];
}
return $perms;
}
/**
* Implementation of hook_menu().
*/
function imagecache_menu($may_cache) {
$items = array();
if ($may_cache) {
// standard imagecache callback.
$items[] = array(
'path' => file_directory_path() .'/imagecache',
'callback' => 'imagecache_cache',
'access' => TRUE,
'type' => MENU_CALLBACK
);
// private downloads imagecache callback
$items[] = array(
'path' => 'system/files/imagecache',
'callback' => 'imagecache_cache_private',
'access' => TRUE,
'type' => MENU_CALLBACK
);
}
return $items;
}
/**
* Implementation of hook_requirements().
*/
function imagecache_requirements($phase) {
$requirements = array();
// Ensure translations don't break at install time.
$t = get_t();
if ($phase == 'runtime') {
// Check for an image library.
if (count(image_get_available_toolkits()) == 0) {
$requirements['clean_urls'] = array(
'title' => $t('Image Toolkit'),
'value' => $t('No image toolkits available'),
'severity' => REQUIREMENT_ERROR,
'description' => $t('Imagecache requires an imagetoolkit such as GD2 or Imagemagick be installed on your server.'),
);
}
// Check for JPEG/PNG/GIF support.
if ('gd' == image_get_toolkit()) {
foreach (array('gif', 'jpeg', 'png') as $format) {
if (!function_exists('imagecreatefrom'. $format)) {
$requirements['gd_'. $format] = array(
'title' => $t('GD !format Support', array('!format' => drupal_ucfirst($format))),
'value' => $t('Not installed'),
'severity' => REQUIREMENT_INFO,
'description' => $t('PHP was not compiled with %format support. Imagecache will not be able to process %format images.', array('%format' => $format)),
);
}
}
}
}
return $requirements;
}
/**
* Implementation of hook_imagecache_actions.
*
* @return array
* An array of information on the actions implemented by a module. The array contains a
* sub-array for each action node type, with the machine-readable action name as the key.
* Each sub-array has up to 3 attributes. Possible attributes:
*
* "name": the human-readable name of the action. Required.
* "description": a brief description of the action. Required.
* "file": the name of the include file the action can be found
* in relative to the implementing module's path.
*/
function imagecache_imagecache_actions() {
$actions = array(
'imagecache_resize' => array(
'name' => 'Resize',
'description' => 'Resize an image to an exact set of dimensions, ignoring aspect ratio.',
),
'imagecache_scale' => array(
'name' => 'Scale',
'description' => 'Resize an image maintaining the original aspect-ratio (only one value necessary).',
'file' => 'imagecache_actions.inc',
),
'imagecache_deprecated_scale' => array(
'name' => 'Deprecated Scale',
'description' => 'Precursor to Scale and Crop. Has inside and outside dimension support. This action will be removed in Imagecache 2.1).',
'file' => 'imagecache_actions.inc',
),
'imagecache_scale_and_crop' => array(
'name' => 'Scale And Crop',
'description' => 'Resize an image to an exact set of dimensions, ignoring aspect ratio.',
'file' => 'imagecache_actions.inc',
),
'imagecache_crop' => array(
'name' => 'Crop',
'description' => 'Crop an image to the rectangle specified by the given offsets and dimensions.',
'file' => 'imagecache_actions.inc',
),
'imagecache_desaturate' => array(
'name' => 'Desaturate',
'description' => 'Convert an image to grey scale.',
'file' => 'imagecache_actions.inc',
),
);
return $actions;
}
/**
* Pull in actions exposed by other modules using hook_imagecache_actions().
*
* @param $reset
* Boolean flag indicating whether the cached data should be
* wiped and recalculated.
*
* @return
* An array of actions to be used when transforming images.
*/
function imagecache_action_definitions($reset = FALSE) {
static $actions;
if (!isset($actions) || $reset) {
if (!$reset && ($cache = cache_get('imagecache_actions')) && !empty($cache->data)) {
$actions = unserialize($cache->data);
}
else {
foreach(module_implements('imagecache_actions') as $module) {
foreach (module_invoke($module, 'imagecache_actions') as $key => $action) {
$action['module'] = $module;
if ($action['file']) {
$action['file'] = drupal_get_path('module', $action['module']) .'/'. $action['file'];
}
$actions[$key] = $action;
};
}
uasort($actions, '_imagecache_definitions_sort');
cache_set('imagecache_actions', 'cache', serialize($actions));
}
}
return $actions;
}
function _imagecache_definitions_sort($a, $b) {
$a = $a['name'];
$b = $b['name'];
if ($a == $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
}
function imagecache_action_definition($action) {
static $definition_cache;
if (!isset($definition_cache[$action])) {
$definitions = imagecache_action_definitions();
$definition = (isset($definitions[$action])) ? $definitions[$action] : array();
if ($definition && $definition['file']) {
require_once($definition['file']);
}
$definition_cache[$action] = $definition;
}
return $definition_cache[$action];
}
/**
* Return a URL that points to the location of a derivative of the
* original image at @p $path, transformed with the given @p $preset.
*/
function imagecache_create_url($presetname, $path) {
$path = _imagecache_strip_file_directory($path);
return file_create_url(file_directory_path() .'/imagecache/'. $presetname .'/'. $path);
}
/**
* Return a file system location that points to the location of a derivative
* of the original image at @p $path, transformed with the given @p $preset.
* Keep in mind that the image might not yet exist and won't be created.
*/
function imagecache_create_path($presetname, $path) {
$path = _imagecache_strip_file_directory($path);
return file_create_path() .'/imagecache/'. $presetname .'/'. $path;
}
/**
* Remove a possible leading file directory path from the given path.
*/
function _imagecache_strip_file_directory($path) {
$dirpath = file_directory_path();
$dirlen = strlen($dirpath);
if (substr($path, 0, $dirlen + 1) == $dirpath .'/') {
$path = substr($path, $dirlen + 1);
}
return $path;
}
/**
* callback for handling public files imagecache requests.
*/
function imagecache_cache() {
$args = func_get_args();
$preset = check_plain(array_shift($args));
$path = implode('/', $args);
_imagecache_cache($preset, $path);
}
/**
* callback for handling private files imagecache requests
*/
function imagecache_cache_private() {
$args = func_get_args();
$preset = check_plain(array_shift($args));
$source = implode('/', $args);
if (user_access('view imagecache '. $preset)) {
_imagecache_cache($preset, $source);
}
else {
// if there is a 40333mage uploaded for the preset display it.
$accesspath = file_create_path('imagecache/'. $preset .'.403.png');
if (file_exists($accesspath)) {
imagecache_transfer($accesspath);
exit;
}
header('HTTP/1.0 403 Forbidden');
exit;
}
}
/**
* handle request validation and responses to imagecache requests.
*/
function _imagecache_cache($presetname, $path) {
if (!$preset = imagecache_preset_by_name($presetname)) {
// send a 404 if we dont' know of a preset.
header("HTTP/1.0 404 Not Found");
exit;
}
$src = file_create_path($path);
if (!is_file($src)) {
// if there is a 404 image uploaded for the preset display it.
$notfoundpath = file_create_path('imagecache/'. $preset['presetname'] .'.404.png');
if (file_exists($notfoundpath)) {
imagecache_transfer($notfoundpath);
exit;
}
// otherwise send a 404.
header("HTTP/1.0 404 Not Found");
exit;
}
$dst = imagecache_create_path($preset['presetname'], $path);
$tmp = file_directory_temp() .'/'. $preset['presetname'] . str_replace(dirname($src) .'/', '', $src);
if (file_exists($tmp)) {
watchdog('imagecache', t('Imagecache already generating: %dst, Lock file: %tmp.', array('%dst' => $dst, '%tmp' => $tmp)), WATCHDOG_NOTICE);
// send a response code that will make the browser wait and reload in a 1/2 sec.
// header()
exit;
}
// check if deriv exists... (file was created between apaches request handler and reaching this code)
// otherwise try to create the derivative.
if (!file_exists($dst) && !imagecache_build_derivative($preset['actions'], $src, $dst, $tmp)) {
// Generate an error if image could not generate.
watchdog('imagecache', t('Failed generating an image from %image using imagecache preset %preset.', array('%image' => $path, '%preset' => $preset)), WATCHDOG_ERROR);
header("HTTP/1.0 500 Internal Server Error");
// remove lock file on error.
unlink($tmp);
exit;
}
imagecache_transfer($dst);
}
function _imagecache_apply_action($action, $image) {
$actions = imagecache_action_definitions();
if ($definition = imagecache_action_definition($action['action'])) {
return call_user_func($action['action'] .'_image', $image, $action['data']);
}
// skip undefined actions.. module probably got uninstalled or disabled.
watchdog('imagecache', t('non-existant action %action', array('%action' => $action['action'])), WATCHDOG_NOTICE);
return TRUE;
}
/**
* helper function to transfer files from imagecache. Determines mime type and sets a last modified header.
* @param $path path to file to be transferred.
* @return
*/
function imagecache_transfer($path) {
if (function_exists('mime_content_type')) {
$mime = mime_content_type($path);
}
else {
$size = getimagesize($path);
$mime = $size['mime'];
}
$headers = array('Content-Type: '. mime_header_encode($mime));
if ($fileinfo = stat($path)) {
$headers[] = 'Last-Modified: '. gmdate('D, d M Y H:i:s', $fileinfo[9]) .' GMT';
}
file_transfer($path, $headers);
exit;
}
/**
* A recursive mkdir.
*/
function _imagecache_mkdir($dir) {
$folders = explode("/", $dir);
foreach ($folders as $folder) {
$dirs[] = $folder;
$path = implode("/", $dirs);
if (is_dir($path)) {
continue;
}
if (is_file($path)) {
watchdog('imagecache', t('file exists where we would like a directory: %path', array('%path' => $path)), WATCHDOG_ERROR);
return FALSE;
}
if (!@mkdir($path)) {
watchdog('imagecache', t('Could not create destination: %dir halted at: %path', array('%dir' => $dir, '%path' => $path)), WATCHDOG_ERROR);
return FALSE;
}
}
return TRUE;
}
/**
* build an image cache derivative
*
* @param $actions, array of imagecache actions.
* @param $source, source file.
* @param $dest, $destination file.
* @return TRUE - derivative generated, FALSE - no derivative generated, NULL - derivative being generated
*/
function imagecache_build_derivative($actions, $src, $dst, $tmp) {
// Copy src to tmp. We use the existence of tmp as a lock.
if (!file_copy($src, $tmp)) {
// can't copy source to temp... ack..
watchdog('imagecache', t('Could not create temporary file: %path', array('%path' => $tmp)), WATCHDOG_ERROR);
return FALSE;
}
// get the folder for the final location of this preset...
$dir = dirname($dst);
// Build the destination folder tree if it doesn't already exists.
if (!file_check_directory($dir) && !_imagecache_mkdir($dir)) {
watchdog('imagecache', t('Failed to create imagecache directory: %dir', array('%dir' => $dir)), WATCHDOG_ERROR);
return FALSE;
}
$image = imageapi_image_open($tmp);
drupal_set_message('loaded image: '. print_r($image, 1));
foreach ($actions as $action) {
if (!empty($action['data'])) {
// QuickSketch, why do these run first/twice? - dopry.
if (isset($action['data']['width'])) {
$width = _imagecache_filter('width', $action['data']['width'], $image->info['width'], $image->info['height']);
}
if (isset($action['data']['height'])) {
$height = _imagecache_filter('height', $action['data']['height'], $image->info['width'], $image->info['height']);
}
foreach ($action['data'] as $key => $value) {
$action['data'][$key] = _imagecache_filter($key, $value, $image->info['width'], $image->info['height'], $width, $height);
}
}
if (!$image = _imagecache_apply_action($action, $image)) {
watchdog( 'imagecache', t('action(id:%id): %action failed for %src', array('%id' => $action['actionid'], '%action' => $action['action'], '%src' => $src)), WATCHDOG_ERROR);
return FALSE;
}
}
imageapi_image_close($image, $tmp);
if (!file_move($tmp, $dst, FILE_EXISTS_ERROR)) {
if (file_exists($dst)) {
watchdog('imagecache', t('Cached image file already exists. There is an issue with your Rewrite configuration.'), WATCHDOG_ERROR);
}
else {
watchdog('imagecache', t('failed to move tmp(%tmp) to dst(%dst)', array('%tmp' => $tmp, '%dst' => $dst)), WATCHDOG_ERROR);
}
return FALSE;
}
return TRUE;
}
/**
* Implementation of hook_field_formatter_info().
*/
function imagecache_field_formatter_info() {
foreach (imagecache_presets() as $preset) {
$formatters[$preset['presetname']] = array(
'label' => $preset['presetname'],
'field types' => array('image'),
);
$formatters[$preset['presetname'] .'_linked'] = array(
'label' => $preset['presetname'] .' as link',
'field types' => array('image'),
);
}
return $formatters;
}
/**
* Implementation of hook_field_formatter().
*/
function imagecache_field_formatter($field, $item, $formatter) {
if (!isset($item['fid'])) {
return '';
}
// Views does not load the file for us, while CCK display fields does.
if (!isset($item['filepath'])) {
$file = _imagecache_file_load($item['fid']);
$item = array_merge($item, $file);
}
$presetname = preg_replace('/_linked$/', '', $formatter);
if ($preset = imagecache_preset_by_name($presetname)) {
return theme('imagecache_formatter', $field, $item, $formatter);
}
}
function _imagecache_file_load($fid = NULL) {
// Don't bother if we weren't passed an fid.
if (isset($fid) && is_numeric($fid)) {
$result = db_query('SELECT * FROM {files} WHERE fid = %d', $fid);
$file = db_fetch_array($result);
}
return ($file) ? $file : array();
}
/**
* Filter key word values such as 'top', 'right', 'center', and also percentages.
* All returned values are in pixels relative to the passed in height and width.
*/
function _imagecache_filter($key, $value, $current_width, $current_height, $new_width = NULL, $new_height = NULL) {
switch ($key) {
case 'width':
$value = _imagecache_percent_filter($value, $current_width);
break;
case 'height':
$value = _imagecache_percent_filter($value, $current_height);
break;
case 'xoffset':
$value = _imagecache_keyword_filter($value, $current_width, $new_width);
break;
case 'yoffset':
$value = _imagecache_keyword_filter($value, $current_height, $new_height);
break;
}
return $value;
}
/**
* Accept a percentage and return it in pixels.
*/
function _imagecache_percent_filter($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 an offset in pixels.
*/
function _imagecache_keyword_filter($value, $current_pixels, $new_pixels) {
switch ($value) {
case 'top':
case 'left':
$value = 0;
break;
case 'bottom':
case 'right':
$value = $current_pixels - $new_pixels;
break;
case 'center':
$value = $current_pixels/2 - $new_pixels/2;
break;
}
return $value;
}
/**
* Recursively delete all files and folders in the specified filepath, then
* delete the containing folder.
*
* Note that this only deletes visible files with write permission.
*
* @param string $path
* A filepath relative to file_directory_path.
*/
function _imagecache_recursive_delete($path) {
$listing = $path .'/*';
foreach (glob($listing) as $file) {
if (is_file($file) === TRUE) {
@unlink($file);
}
elseif (is_dir($file) === TRUE) {
_imagecache_recursive_delete($file);
}
}
@rmdir($path);
}
/**
* Theme an img tag for displaying the image.
*/
function theme_imagecache_display($node, $label, $url, $attributes) {
return '';
}
function theme_imagecache_formatter($field, $item, $formatter) {
if (preg_match('/_linked$/', $formatter)) {
$formatter = preg_replace('/_linked$/', '', $formatter);
$image = theme('imagecache', $formatter, $item['filepath'], $item['alt'], $item['title']);
$output = l($image, 'node/'. $item['nid'], array(), NULL, NULL, FALSE, TRUE);
}
else {
$output = theme('imagecache', $formatter, $item['filepath'], $item['alt'], $item['title']);
}
return $output;
}
function theme_imagecache($namespace, $path, $alt = '', $title = '', $attributes = NULL) {
$attributes = drupal_attributes($attributes);
$imagecache_path = imagecache_create_url($namespace, $path);
return '';
}
/************************************************************************************
* ImageCache action implementation example in module.
*/
function imagecache_resize_image($image, $data) {
if (!$image = imageapi_resize($image, $data['width'], $data['height'])) {
watchdog('imagecache', t('imagecache_resize_image failed. image: %image, data: %data.', array('%path' => $image, '%data' => print_r($data, TRUE))), WATCHDOG_ERROR);
return FALSE;
}
return $image;
}
function imagecache_resize_form($action) {
$form['width'] = array(
'#type' => 'textfield',
'#title' => t('Width'),
'#default_value' => $action['width'],
'#description' => t('Enter a width in pixels or as a percentage. i.e. 500 or 80%.'),
);
$form['height'] = array(
'#type' => 'textfield',
'#title' => t('Height'),
'#default_value' => $action['height'],
'#description' => t('Enter a height in pixels or as a percentage. i.e. 500 or 80%.'),
);
return $form;
}
function theme_imagecache_resize($element) {
$data = $element['#value'];
return 'width: '. $data['width'] .', height: '. $data['height'];
}
/**
* ImageCache 2.x API
*
* The API for imagecache has changed. There is a compatibility layer for
* imagecache 1.x. Please see the imagecache_compat.module
*
* The 2.x API returns more structured data, has shorter function names, and
* implements more aggressive metadata caching.
*
*/
/**
* Get an array of all presets and their settings.
*
* @param reset
* if set to true it will clear the preset cache
*
* @return
* array of presets array( $preset_id => array('presetid' => integer, 'presetname' => string))
*/
function imagecache_presets($reset = FALSE) {
static $presets = array();
// Clear caches if $reset is TRUE;
if ($reset) {
$presets = array();
cache_clear_all('imagecache:presets', 'cache');
// Clear the content.module cache (refreshes the list of formatters provided by imagefield.module).
if (module_exists('content')) {
content_clear_type_cache();
}
}
// Return presets if the array is populated.
if (!empty($presets)) {
return $presets;
}
// Grab from cache or build the array.
if ($cache = cache_get('imagecache:presets', 'cache')) {
$presets = unserialize($cache->data);
}
else {
$result = db_query('SELECT * FROM {imagecache_preset} ORDER BY presetname');
while ($preset = db_fetch_array($result)) {
$presets[$preset['presetid']] = $preset;
$presets[$preset['presetid']]['actions'] = imagecache_preset_actions($preset);
}
cache_set('imagecache:presets', 'cache', serialize($presets));
}
return $presets;
}
/**
* Load a preset by preset_id.
*
* @param preset_id
* The numeric id of a preset.
*
* @return
* preset array( 'presetname' => string, 'presetid' => integet)
* empty array if preset_id is an invalid preset
*/
function imagecache_preset($preset_id, $reset = FALSE) {
$presets = imagecache_presets($reset);
return (isset($presets[$preset_id])) ? $presets[$preset_id] : array();
}
/**
* Load a preset by name.
*
* @param preset_name
*
* @return
* preset array( 'presetname' => string, 'presetid' => integer)
* empty array if preset_name is an invalid preset
*/
function imagecache_preset_by_name($preset_name) {
static $presets_by_name = array();
if (!$presets_by_name && $presets = imagecache_presets()) {
foreach ($presets as $preset) {
$presets_by_name[$preset['presetname']] = $preset;
}
}
return (isset($presets_by_name[$preset_name])) ? $presets_by_name[$preset_name] : array();
}
/**
* Save an ImageCache preset.
*
* @param preset
* an imagecache preset array.
* @return
* a preset array. In the case of a new preset, 'presetid' will be populated.
*/
function imagecache_preset_save($preset) {
// @todo: CRUD level validation?
if (isset($preset['presetid']) && is_numeric($preset['presetid'])) {
db_query('UPDATE {imagecache_preset} SET presetname =\'%s\' WHERE presetid = %d', $preset['presetname'], $preset['presetid']);
}
else {
$preset['presetid'] = db_next_id('{imagecache_preset}_presetid');
db_query('INSERT INTO {imagecache_preset} (presetid, presetname) VALUES (%d, \'%s\')', $preset['presetid'], $preset['presetname']);
}
// Reset presets cache.
imagecache_preset_flush($preset);
imagecache_presets(TRUE);
return $preset;
}
function imagecache_preset_delete($preset) {
imagecache_preset_flush($preset['presetid']);
db_query('DELETE FROM {imagecache_action} where presetid = %d', $preset['presetid']);
db_query('DELETE FROM {imagecache_preset} where presetid = %d', $preset['presetid']);
imagecache_presets(TRUE);
return TRUE;
}
function imagecache_preset_actions($preset, $reset = FALSE) {
static $actions_cache = array();
if ($reset || empty($actions_cache[$preset['presetid']])) {
$result = db_query('SELECT * FROM {imagecache_action} where presetid = %d order by weight', $preset['presetid']);
while ($row = db_fetch_array($result)) {
$row['data'] = unserialize($row['data']);
$actions_cache[$preset['presetid']][] = $row;
}
}
return isset($actions_cache[$preset['presetid']]) ? $actions_cache[$preset['presetid']] : array();
}
/**
* Flush cached media for a preset.
* @param id
* A preset id.
*/
function imagecache_preset_flush($preset) {
if (user_access('flush imagecache')) {
$presetdir = realpath(file_directory_path() .'/imagecache/'. $preset['presetname']);
if (is_dir($presetdir)) {
_imagecache_recursive_delete($presetdir);
}
}
}
/**
* Clear cached versions of a specific file in all presets.
* @param $path
* The Drupal file path to the original image.
*/
function imagecache_image_flush($path) {
foreach (imagecache_presets() as $preset) {
$ipath = file_directory_path() .'/imagecache/'. $preset['presetname'] .'/'. $path;
file_delete($ipath);
}
}
function imagecache_action($actionid) {
static $actions;
if (!isset($actions[$actionid])) {
$action = array();
$result = db_query('SELECT * FROM {imagecache_action} WHERE actionid=%d', $actionid);
if ($row = db_fetch_array($result)) {
$action = $row;
$action['data'] = unserialize($action['data']);
$definition = imagecache_action_definition($action['action']);
$action = array_merge($definition, $action);
$actions[$actionid] = $action;
}
}
return $actions[$actionid];
}
function imagecache_action_save($action) {
if ($action['actionid']) {
db_query("UPDATE {imagecache_action} SET weight=%d, data='%s' WHERE actionid=%d", $action['weight'], serialize($action['data']), $action['actionid']);
}
else {
$action['actionid'] = db_next_id('{imagecache_action}_actionid');
db_query("INSERT INTO {imagecache_action} (actionid, presetid, weight, action, data) VALUES (%d, %d, %d,'%s', '%s')", $action['actionid'], $action['presetid'], $action['weight'], $action['action'], serialize($action['data']));
}
$preset = imagecache_preset($action['presetid']);
imagecache_preset_flush($preset);
imagecache_presets(TRUE);
return $action;
}
function imagecache_action_delete($action) {
db_query("DELETE FROM {imagecache_action} WHERE actionid=%d", $action['actionid']);
$preset = imagecache_preset($action['presetid']);
imagecache_preset_flush($preset);
imagecache_presets(TRUE);
}