/path * * Notes: * To add a new handler, * add fields and select option to _imagecache_actions_form; * add handling code to imagecache_cache * */ /** * Implementation of hook_perm(). */ function imagecache_perm() { return array('administer imagecache', 'flush imagecache'); } /** * Implementation of hook_menu(). */ function imagecache_menu($may_cache) { $items = array(); if ($may_cache) { $items[] = array( 'path' => file_directory_path() .'/imagecache', 'callback' => 'imagecache_cache', 'access' => TRUE, 'type' => MENU_CALLBACK ); $items[] = array( 'path' => 'admin/settings/imagecache', 'title' => t('Image cache'), 'description' => t('Configure rulesets and actions for imagecache.'), 'access' => user_access('administer imagecache'), 'callback' => 'drupal_get_form', 'callback arguments' => array('imagecache_admin'), ); } 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') { // Clean URLS. if (!variable_get('clean_url', 0)) { $requirements['clean_urls'] = array( 'title' => $t('Clean URLs'), 'value' => $t('Not enabled'), 'severity' => REQUIREMENT_ERROR, 'description' => $t('Imagecache will not operate properly if Clean URLs is not enabled on your site.', array('!url' => url('admin/settings/clean-urls'))), ); } // Check for an image library. if (count(image_get_available_toolkits()) == 0) { $requirements['image_toolkits'] = 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 public files. if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE) { $requirements['file_system'] = array( 'title' => $t('File Download Method'), 'value' => $t('Private Downloads'), 'severity' => REQUIREMENT_ERROR, 'description' => $t('Imagecache will not operate properly using Private Files. Please enable Public File Transfer.', array('!url' => url('admin/settings/file-system'))), ); } // 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; } function imagecache_cache() { $args = func_get_args(); $preset = array_shift($args); $preset_id = _imagecache_preset_load_by_name($preset); if (!$preset_id) { watchdog('imagecache', t('Preset(%p) not found', array('%p' => $preset)), WATCHDOG_ERROR); header('HTTP/1.0 404 Not Found'); exit; } // Verify that the source exists, if not exit. Maybe display a missing image. $src = file_create_path(implode('/', $args)); if (!is_file($src)) { watchdog('imagecache', t('Source(%s) not found.', array('%s' => $src)), WATCHDOG_ERROR); header('HTTP/1.0 404 Not Found'); exit; } // Build the destination folder tree if it doesn't already exists, // and make sure it is writable. $dst = 'imagecache/'. $preset .'/'. $src; // Build the destination folder tree if it doesn't already exists. if (!file_check_directory(file_create_path($dst))) { $folders = explode("/", _imagecache_strip_file_directory(dirname($dst))); foreach ($folders as $folder) { $tpath[] = $folder; $checkpath = implode("/", $tpath); if (!file_check_directory(file_create_path($checkpath), FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) { watchdog('imagecache', t('Cannot create/fix dir(%d).', array('%d' => $checkpath)), WATCHDOG_ERROR); header("HTTP/1.0 500 Internal Server Error"); exit; } } } // Prepend presetname to tmp file name to prevent namespace clashes when // multiple presets for the same image are called in rapid succession. $tmp = file_directory_temp() .'/tmp_imagecache_'. md5($src) .'_'. $preset .'_'. basename($src); register_shutdown_function('file_delete', $tmp); // Check if file exists to prevent multiple apache children from trying to generate. if (is_file($tmp)) { watchdog('imagecache', t('tmp file(%t) already exists.', array('%t' => $tmp)), WATCHDOG_NOTICE); // non-caching redirect to self, we hope the processing has finished by the time we come back. // 307 Temporary Redirect header("Location: ". $_SERVER['REQUEST_URI'], TRUE, 307); exit; } // copy source to a temporary file we will use to apply the imagecache actions to. if (!file_copy($src, $tmp)) { watchdog('imagecache', t('Could not copy src(%s) to tmp(%t).', array('%s' => $src, '%t' => $tmp)), WATCHDOG_ERROR); header("HTTP/1.0 500 Internal Server Error"); exit; } $actions = _imagecache_actions_get_by_presetid($preset_id); foreach ($actions as $action) { $size = getimagesize($tmpdestination); if (empty($action['data']['height'])) $action['data']['height'] = round($size[1] * ($action['data']['width'] / $size[0])); if (empty($action['data']['width'])) $action['data']['width'] = $size[0] * ($action['data']['height'] / $size[1]); $new_width = _imagecache_filter('width', $action['data']['width'], $size[0], $size[1]); $new_height = _imagecache_filter('height', $action['data']['height'], $size[0], $size[1]); foreach ($action['data'] as $key => $value) $action['data'][$key] = _imagecache_filter($key, $value, $size[0], $size[1], $new_width, $new_height); switch ($action['data']['function']) { case 'resize': if (!image_resize($tmp, $tmp, $action['data']['width'], $action['data']['height'])) watchdog('imagecache', t('Imagecache resize action ID %id failed.', array('%id' => $action['actionid'])), WATCHDOG_ERROR); break; case 'scale': if ($action['data']['fit'] == 'outside' && $action['data']['width'] && $action['data']['height']) { $ratio = $size[0]/$size[1]; $new_ratio = $action['data']['width']/$action['data']['height']; $action['data']['width'] = $ratio > $new_ratio ? 0 : $action['data']['width']; $action['data']['height'] = $ratio < $new_ratio ? 0 : $action['data']['height']; } // Set impossibly large values if the width and height aren't set. $action['data']['width'] = $action['data']['width'] ? $action['data']['width'] : 9999999; $action['data']['height'] = $action['data']['height'] ? $action['data']['height'] : 9999999; if (!image_scale($tmp, $tmp, $action['data']['width'], $action['data']['height'])) watchdog('imagecache', t('Imagecache scale action ID %id failed.', array('%id' => $action['actionid'])), WATCHDOG_ERROR); break; case 'crop': if (!image_crop($tmp, $tmp, $action['data']['xoffset'], $action['data']['yoffset'], $action['data']['width'], $action['data']['height'])) watchdog('imagecache', t('Imagecache crop action ID %id failed.', array('%id' => $action['actionid'])), WATCHDOG_ERROR); break; } } // move the temporary file to it's final destination. if (!file_move($tmp, file_create_path($dst))) { watchdog('imagecache', t('Could not move tmp(%t) to dst(%d).', array('%d' => $dst, '%t' => $tmp)), WATCHDOG_ERROR); header("HTTP/1.0 500 Internal Server Error"); exit; } // Serve it up... $size = getimagesize($dst); file_transfer($dst, array('Content-Type: '. mime_header_encode($size['mime']), 'Content-Length: '. filesize($dst))); exit; } /** * 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; } /** * Implementation of hook_field_formatter_info(). */ function imagecache_field_formatter_info() { $rules = _imagecache_get_presets(); foreach ($rules as $ruleid => $rulename) { $formatters[$rulename] = array( 'label' => t('!rulename as image', array('!rulename' => $rulename)), 'field types' => array('image'), ); $formatters[$rulename .'_linked'] = array( 'label' => t('!rulename as link', array('!rulename' => $rulename)), 'field types' => array('image'), ); } return $formatters; } /** * Implementation of hook_field_formatter(). */ function imagecache_field_formatter($field, $item, $formatter) { if (empty($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); } $rules = _imagecache_get_presets(); $formatter_check = preg_replace('/_linked$/', '', $formatter); if (in_array($formatter_check, (array) $rules)) { 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(); } function _imagecache_get_presets($reset = FALSE) { static $presets = array(); // Check caches if $reset is FALSE; if (!$reset) { if (!empty($presets)) { return $presets; } // Grab from cache saves building the array. // Plus it's a frequently used table. $cache = cache_get('imagecache:presets', 'cache'); $presets = unserialize($cache->data); // If the preset is not an array, cache_clear_all has been called // there no/invalid data in the cache. Fall through and repopulate cache; if (is_array($presets)) { return $presets; } } // Load Data from the database on reset or if we get invalid data from the array. $presets = array(); $result = db_query('SELECT presetid, presetname FROM {imagecache_preset} ORDER BY presetname'); while ($row = db_fetch_array($result)) { $presets[$row['presetid']] = $row['presetname']; } cache_set('imagecache:presets', 'cache', serialize($presets)); // Clear the content.module cache (refreshes the list of formatters provided by imagefield.module). if (module_exists('content')) { content_clear_type_cache(); } return $presets; } function _imagecache_actions_get_by_presetid($presetid) { $actions = array(); $result = db_query('SELECT actionid, weight, data FROM {imagecache_action} where presetid = %d order by weight', $presetid); while ($row = db_fetch_array($result)) { $row['data'] = unserialize($row['data']); $actions[$row['actionid']] = $row; } return $actions; } /** * 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; } function imagecache_admin() { drupal_set_title('Imagecache administration'); $form = array(); $form['title'] = array('#type' => 'markup', '#value' => t('Imagecache presets')); $form['presets']['#tree'] = TRUE; $presets = _imagecache_get_presets(); foreach ($presets as $presetid => $presetname) { $form['presets'][$presetid] = array( '#type' => 'fieldset', '#title' => $presetname, '#collapsible' => TRUE, '#collapsed' => arg(4) != $presetid, ); $form['presets'][$presetid]['name'] = array( '#type' => 'textfield', '#title' => t('Preset namespace'), '#default_value' => $presetname, '#description' => t('String that will be used as an identifier in the url for this set of handlers. Final urls will look like http://example.com/files/imagecache/%namespace/<path to orig>', array('%namespace' => $presetname)), ); $form['presets'][$presetid]['handlers'] = array( '#type' => 'fieldset', '#title' => t('Image handlers'), ); $form['presets'][$presetid]['handlers']['#tree'] = FALSE; $form['presets'][$presetid]['handlers'] = _imagecache_actions_form($presetid); $form['presets'][$presetid]['ops']['#tree'] = FALSE; $form['presets'][$presetid]['ops']['update'] = array( '#type' => 'submit', '#name' => 'preset-op['. $presetid .']', '#value' => t('Update preset'), ); $form['presets'][$presetid]['ops']['delete'] = array( '#type' => 'submit', '#name' => 'preset-op['. $presetid .']', '#value' => t('Delete preset'), ); $form['presets'][$presetid]['ops']['flush'] = array( '#type' => 'submit', '#name' => 'preset-op['. $presetid .']', '#value' => t('Flush preset images'), ); } $form['presets']['new'] = array( '#type' => 'fieldset', '#title' => t('New preset'), '#tree' => TRUE, ); $form['presets']['new']['name'] = array( '#type' => 'textfield', '#size' => '64', '#title' => t('Preset namespace'), '#default_value' => '', '#description' => t('The namespace of an imagecache preset. It represents a series of actions to be performed when imagecache dynamically generates an image. This will also be used in the url for images. Please no spaces.'), ); $form['presets']['new']['create'] = array( '#type' => 'submit', '#name' => 'preset-op[new]', '#value' => t('Create preset'), '#weight' => 10, ); return $form; } function imagecache_admin_validate($form_id, $form_values) { if (is_array($_POST['preset-op'])) { foreach ($_POST['preset-op'] as $presetid => $op) { // Check for illegal characters in preset names if (preg_match('/[^0-9a-zA-Z_\-]/', $form_values['presets'][$presetid]['name'])) { form_set_error('presets]['. $presetid .'][name', t('Please only use alphanumic characters, underscores (_), and hyphens (-) for preset names.')); } } } } function imagecache_admin_submit($form_id, $form_values) { if (is_array($_POST['preset-op'])) { foreach ($_POST['preset-op'] as $presetid => $op) { $presetid = check_plain($presetid); switch ($op) { case t('Create preset'): _imagecache_preset_create($form_values['presets']['new']['name']); break; case t('Update preset'): // Add new actions $newaction = $form_values['presets'][$presetid]['handlers']['newaction']; if ($newaction) { $action = array(); $action['data'] = array('function' => $newaction); $action['presetid'] = $presetid; $action['weight'] = 0; _imagecache_action_create($action); } // Update existing actions foreach ($form_values['presets'][$presetid]['handlers'] as $actionid => $action) { if ($actionid != 'newaction') { $action['actionid'] = $actionid; $action['presetid'] = $presetid; $remove = $action['remove']; unset($action['remove']); $remove ? _imagecache_action_delete($action) : _imagecache_action_update($action); } } // Update the entire preset. _imagecache_preset_update($presetid, $form_values['presets'][$presetid]['name']); break; case t('Delete preset'): _imagecache_preset_delete($presetid, $form_values['presets'][$presetid]['name']); break; case t('Flush preset images'): _imagecache_preset_flush($presetid); break; } } } } /** * Load a preset by id. * @param id * Preset id. */ function _imagecache_preset_load($id) { $presets = _imagecache_get_presets(); return $presets[$id]; } /** * Load a preset by name. * @param name * Preset name. */ function _imagecache_preset_load_by_name($name) { $presets = array_flip(_imagecache_get_presets()); return $presets[$name]; } /** * Create a preset. * @param name * Name of the preset to be created. */ function _imagecache_preset_create($name) { $next_id = db_next_id('{imagecache_preset}_presetid'); db_query('INSERT INTO {imagecache_preset} (presetid, presetname) VALUES (%d, \'%s\')', $next_id, $name); // Reset presets cache. _imagecache_get_presets(TRUE); $_REQUEST['destination'] = 'admin/settings/imagecache/preset/'. $next_id; } /** * Update a preset. * @param id * Preset id. * @param name * new name for the preset */ function _imagecache_preset_update($id, $name) { $name = check_plain($name); $id = (int)$id; _imagecache_preset_flush($id); db_query('UPDATE {imagecache_preset} SET presetname =\'%s\' WHERE presetid = %d', $name, $id); drupal_set_message(t('Updated preset "%name" (ID: @id)', array('%name' => $name, '@id' => $id))); // Reset presets cache. _imagecache_get_presets(TRUE); $_REQUEST['destination'] = 'admin/settings/imagecache/preset/'. $id; } function _imagecache_preset_delete($id, $name) { _imagecache_preset_flush($id); db_query('DELETE FROM {imagecache_action} where presetid = %d', $id); db_query('DELETE FROM {imagecache_preset} where presetid = %d', $id); drupal_set_message(t('Preset "%name" (ID: @id) deleted.', array('%name' => $name, '@id' => $id))); // Reset presets cache. _imagecache_get_presets(TRUE); } /** * Flush cached media for a preset. * @param id * A preset id. */ function _imagecache_preset_flush($id) { if (user_access('flush imagecache')) { drupal_set_message(t('Flushed Preset Images (ID: @id)', array('@id' => $id))); $preset = _imagecache_preset_load($id); $presetdir = realpath(file_directory_path() .'/imagecache/'. $preset); if (is_dir($presetdir)) { _imagecache_recursive_delete($presetdir); } } } /** * 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); } function _imagecache_action_create($action) { //debug_msg($action, 'action@create: '); $next_id = db_next_id('{imagecache_action}_actionid'); db_query('INSERT INTO {imagecache_action} (actionid, presetid, weight, data) VALUES (%d, %d, %d, \'%s\')', $next_id, $action['presetid'], $action['weight'], serialize($action['data'])); } function _imagecache_action_update($action) { //debug_msg($action, 'action@update'); db_query('UPDATE {imagecache_action} SET weight = %d, data = \'%s\' WHERE actionid = %d', $action['weight'], serialize($action['data']), $action['actionid']); } function _imagecache_action_delete($action) { _imagecache_preset_flush($action['presetid']); db_query('DELETE FROM {imagecache_action} WHERE actionid = %d', $action['actionid']); } function _imagecache_actions_form($presetid) { $form = array(); $actions = _imagecache_actions_get_by_presetid($presetid); foreach ($actions as $actionid => $action) { //debug_msg($action); $form[$actionid] = array( '#type' => 'fieldset', '#title' => t($action['data']['function']), ); $form[$actionid]['data']['function'] = array( '#type' => 'hidden', '#value' => $action['data']['function'], ); $form[$actionid]['weight'] = array( '#type' => 'weight', '#title' => t('Weight'), '#default_value' => $action['weight'], ); switch ($action['data']['function']) { case 'scale': $helptext = array(); $helptext['inside'] = t('Inside dimensions: Final dimensions will be less than or equal to the entered width and height. Useful for ensuring a maximum height and/or width.'); $helptext['outside'] = t('Outside dimensions: Final dimensions will be greater than or equal to the entered width and height. Ideal for cropping the result to a square.'); $description = '