1ab1273f535552e8abeb74adc180820df2b42597
[project/imagecache.git] / imagecache.module
1 <?php
2 // $Id$
3
4 /**
5 * @file
6 * Dynamic image resizer and image cacher.
7 *
8 * ImageCache allows you to setup presets for image processing.
9 * If an ImageCache derivative doesn't exist the web server's
10 * rewrite rules will pass the request to Drupal which in turn
11 * hands it off to imagecache to dynamically generate the file.
12 *
13 * To view a derivative image you request a special url containing
14 * 'imagecache/<presetname>/path/to/file.ext.
15 *
16 * Presets can be managed at http://example.com/admin/build/imagecache.
17 *
18 * To view a derivative image you request a special url containing
19 * 'imagecache/<presetname>/path/to/file.ext.
20 *
21 * If you had a preset names 'thumbnail' and you wanted to see the
22 * thumbnail version of http://example.com/files/path/to/myimage.jpg you
23 * would use http://example.com/files/imagecache/thumbnail/path/to/myimage.jpg
24 *
25 * ImageCache provides formatters for CCK Imagefields and is leveraged by several
26 * other modules. ImageCache also relies heavily on ImageAPI for it's image processing.
27 * If there are errors with actual image processing look to ImageAPI first.
28 *
29 * @todo: add watermarking capabilities.
30 *
31 */
32
33 /**
34 * Imagecache preset storage constant for user-defined presets in the DB.
35 */
36 define('IMAGECACHE_STORAGE_NORMAL', 0);
37
38 /**
39 * Imagecache preset storage constant for module-defined presets in code.
40 */
41 define('IMAGECACHE_STORAGE_DEFAULT', 1);
42
43 /**
44 * Imagecache preset storage constant for user-defined presets that override
45 * module-defined presets.
46 */
47 define('IMAGECACHE_STORAGE_OVERRIDE', 2);
48
49 /*********************************************************************************************
50 * Drupal Hooks
51 *********************************************************************************************/
52
53 /**
54 * Implementation of hook_perm().
55 */
56 function imagecache_perm() {
57 $perms = array('administer imagecache', 'flush imagecache');
58 foreach (imagecache_presets() as $preset) {
59 $perms[] = 'view imagecache '. $preset['presetname'];
60 }
61 return $perms;
62 }
63
64 /**
65 * Implementation of hook_menu().
66 */
67 function imagecache_menu() {
68 $items = array();
69
70 // standard imagecache callback.
71 $items[file_directory_path() .'/imagecache'] = array(
72 'page callback' => 'imagecache_cache',
73 'access callback' => TRUE,
74 'type' => MENU_CALLBACK
75 );
76 // private downloads imagecache callback
77 $items['system/files/imagecache'] = array(
78 'page callback' => 'imagecache_cache_private',
79 'access callback' => TRUE,
80 'type' => MENU_CALLBACK
81 );
82
83 return $items;
84 }
85
86 /**
87 * Clear imagecache presets cache on admin/build/modules form.
88 */
89 function imagecache_form_system_modules_alter(&$form, $form_state) {
90 imagecache_presets(TRUE);
91 }
92
93 /**
94 * Implementation of hook_theme().
95 */
96 function imagecache_theme() {
97 $theme = array(
98 'imagecache' => array(
99 'arguments' => array(
100 'namespace' => NULL,
101 'path' => NULL,
102 'alt' => NULL,
103 'title' => NULL,
104 )),
105 'imagecache_imagelink' => array(
106 'arguments' => array(
107 'namespace' => NULL,
108 'path' => NULL,
109 'alt' => NULL,
110 'title' => NULL,
111 'attributes' => array(),
112 )),
113 'imagecache_resize' => array(
114 'file' => 'imagecache_actions.inc',
115 'arguments' => array('element' => NULL),
116 ),
117 'imagecache_scale' => array(
118 'file' => 'imagecache_actions.inc',
119 'arguments' => array('element' => NULL),
120 ),
121 'imagecache_scale_and_crop' => array(
122 'file' => 'imagecache_actions.inc',
123 'arguments' => array('element' => NULL),
124 ),
125 'imagecache_deprecated_scale' => array(
126 'file' => 'imagecache_actions.inc',
127 'arguments' => array('element' => NULL),
128 ),
129 'imagecache_crop' => array(
130 'file' => 'imagecache_actions.inc',
131 'arguments' => array('element' => NULL),
132 ),
133 'imagecache_desaturate' => array(
134 'file' => 'imagecache_actions.inc',
135 'arguments' => array('element' => NULL),
136 ),
137 'imagecache_rotate' => array(
138 'file' => 'imagecache_actions.inc',
139 'arguments' => array('element' => NULL),
140 ),
141 'imagecache_sharpen' => array(
142 'file' => 'imagecache_actions.inc',
143 'arguments' => array('element' => NULL),
144 ),
145 );
146
147 foreach (imagecache_presets() as $preset) {
148 $theme['imagecache_formatter_'. $preset['presetname'] .'_default'] = array(
149 'arguments' => array('element' => NULL),
150 'function' => 'theme_imagecache_formatter_default',
151 );
152 $theme['imagecache_formatter_'. $preset['presetname'] .'_linked'] = array(
153 'arguments' => array('element' => NULL),
154 'function' => 'theme_imagecache_formatter_linked',
155 );
156 $theme['imagecache_formatter_'. $preset['presetname'] .'_imagelink'] = array(
157 'arguments' => array('element' => NULL),
158 'function' => 'theme_imagecache_formatter_imagelink',
159 );
160 $theme['imagecache_formatter_'. $preset['presetname'] .'_path'] = array(
161 'arguments' => array('element' => NULL),
162 'function' => 'theme_imagecache_formatter_path',
163 );
164 $theme['imagecache_formatter_'. $preset['presetname'] .'_url'] = array(
165 'arguments' => array('element' => NULL),
166 'function' => 'theme_imagecache_formatter_url',
167 );
168 }
169
170 return $theme;
171
172 }
173
174 /**
175 * Implementation of hook_imagecache_actions.
176 *
177 * @return array
178 * An array of information on the actions implemented by a module. The array
179 * contains a sub-array for each action node type, with the machine-readable
180 * action name as the key. Each sub-array has up to 3 attributes. Possible
181 * attributes:
182 *
183 * "name": the human-readable name of the action. Required.
184 * "description": a brief description of the action. Required.
185 * "file": the name of the include file the action can be found
186 * in relative to the implementing module's path.
187 */
188 function imagecache_imagecache_actions() {
189 $actions = array(
190 'imagecache_resize' => array(
191 'name' => 'Resize',
192 'description' => 'Resize an image to an exact set of dimensions, ignoring aspect ratio.',
193 'file' => 'imagecache_actions.inc',
194 ),
195 'imagecache_scale' => array(
196 'name' => 'Scale',
197 'description' => 'Resize an image maintaining the original aspect-ratio (only one value necessary).',
198 'file' => 'imagecache_actions.inc',
199 ),
200 'imagecache_deprecated_scale' => array(
201 'name' => 'Deprecated Scale',
202 'description' => 'Precursor to Scale and Crop. Has inside and outside dimension support. This action will be removed in ImageCache 2.1).',
203 'file' => 'imagecache_actions.inc',
204 ),
205 'imagecache_scale_and_crop' => array(
206 'name' => 'Scale And Crop',
207 'description' => 'Resize an image while maintaining aspect ratio, then crop it to the specified dimensions.',
208 'file' => 'imagecache_actions.inc',
209 ),
210 'imagecache_crop' => array(
211 'name' => 'Crop',
212 'description' => 'Crop an image to the rectangle specified by the given offsets and dimensions.',
213 'file' => 'imagecache_actions.inc',
214 ),
215 'imagecache_desaturate' => array(
216 'name' => 'Desaturate',
217 'description' => 'Convert an image to grey scale.',
218 'file' => 'imagecache_actions.inc',
219 ),
220 'imagecache_rotate' => array(
221 'name' => 'Rotate',
222 'description' => 'Rotate an image.',
223 'file' => 'imagecache_actions.inc',
224 ),
225 'imagecache_sharpen' => array(
226 'name' => 'Sharpen',
227 'description' => 'Sharpen an image using unsharp masking.',
228 'file' => 'imagecache_actions.inc',
229 ),
230 );
231
232 return $actions;
233 }
234
235 /**
236 * Pull in actions exposed by other modules using hook_imagecache_actions().
237 *
238 * @param $reset
239 * Boolean flag indicating whether the cached data should be
240 * wiped and recalculated.
241 *
242 * @return
243 * An array of actions to be used when transforming images.
244 */
245 function imagecache_action_definitions($reset = FALSE) {
246 static $actions;
247 if (!isset($actions) || $reset) {
248 if (!$reset && ($cache = cache_get('imagecache_actions')) && !empty($cache->data)) {
249 $actions = $cache->data;
250 }
251 else {
252 foreach (module_implements('imagecache_actions') as $module) {
253 foreach (module_invoke($module, 'imagecache_actions') as $key => $action) {
254 $action['module'] = $module;
255 if (!empty($action['file'])) {
256 $action['file'] = drupal_get_path('module', $action['module']) .'/'. $action['file'];
257 }
258 $actions[$key] = $action;
259 };
260 }
261 uasort($actions, '_imagecache_definitions_sort');
262 cache_set('imagecache_actions', $actions);
263 }
264 }
265 return $actions;
266 }
267
268 function _imagecache_definitions_sort($a, $b) {
269 $a = $a['name'];
270 $b = $b['name'];
271 if ($a == $b) {
272 return 0;
273 }
274 return ($a < $b) ? -1 : 1;
275 }
276
277 function imagecache_action_definition($action) {
278 static $definition_cache;
279 if (!isset($definition_cache[$action])) {
280 $definitions = imagecache_action_definitions();
281 $definition = (isset($definitions[$action])) ? $definitions[$action] : array();
282
283 if (isset($definition['file'])) {
284 require_once($definition['file']);
285 }
286 $definition_cache[$action] = $definition;
287 }
288 return $definition_cache[$action];
289 }
290
291 /**
292 * Return a URL that points to the location of a derivative of the
293 * original image transformed with the given preset.
294 *
295 * Special care is taken to make this work with the possible combinations of
296 * Clean URLs and public/private downloads. For example, when Clean URLs are not
297 * available an URL with query should be returned, like
298 * http://example.com/?q=files/imagecache/foo.jpg, so that imagecache is able
299 * intercept the request for this file.
300 *
301 * This code is very similar to the Drupal core function file_create_url(), but
302 * handles the case of Clean URLs and public downloads differently however.
303 *
304 * @param $presetname
305 * @param $filepath
306 * String specifying the path to the image file.
307 * @param $bypass_browser_cache
308 * A Boolean indicating that the URL for the image should be distinct so that
309 * the visitors browser will not be able to use a previously cached version.
310 * Defaults to FALSE.
311 * @param $absolute
312 * A Boolean indicating that the URL should be absolute. Defaults to TRUE.
313 */
314 function imagecache_create_url($presetname, $filepath, $bypass_browser_cache = FALSE, $absolute = TRUE) {
315 $path = _imagecache_strip_file_directory($filepath);
316 if (module_exists('transliteration')) {
317 $path = transliteration_get($path);
318 }
319
320 $args = array('absolute' => $absolute, 'query' => $bypass_browser_cache ? time() : $bypass_browser_cache);
321 switch (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC)) {
322 case FILE_DOWNLOADS_PUBLIC:
323 $base = $absolute ? $GLOBALS['base_url'] .'/' : '';
324 return url($base . file_directory_path() ."/imagecache/$presetname/$path", $args);
325 case FILE_DOWNLOADS_PRIVATE:
326 return url("system/files/imagecache/$presetname/$path", $args);
327 }
328 }
329
330 /**
331 * Return a file system location that points to the location of a derivative
332 * of the original image at @p $path, transformed with the given @p $preset.
333 * Keep in mind that the image might not yet exist and won't be created.
334 */
335 function imagecache_create_path($presetname, $path) {
336 $path = _imagecache_strip_file_directory($path);
337 return file_create_path() .'/imagecache/'. $presetname .'/'. $path;
338 }
339
340 /**
341 * Remove a possible leading file directory path from the given path.
342 */
343 function _imagecache_strip_file_directory($path) {
344 $dirpath = file_directory_path();
345 $dirlen = strlen($dirpath);
346 if (substr($path, 0, $dirlen + 1) == $dirpath .'/') {
347 $path = substr($path, $dirlen + 1);
348 }
349 return $path;
350 }
351
352
353 /**
354 * callback for handling public files imagecache requests.
355 */
356 function imagecache_cache() {
357 $args = func_get_args();
358 $preset = check_plain(array_shift($args));
359 $path = implode('/', $args);
360 _imagecache_cache($preset, $path);
361 }
362
363 /**
364 * callback for handling private files imagecache requests
365 */
366 function imagecache_cache_private() {
367 $args = func_get_args();
368 $preset = check_plain(array_shift($args));
369 $source = implode('/', $args);
370
371 if (user_access('view imagecache '. $preset)) {
372 _imagecache_cache($preset, $source);
373 }
374 else {
375 // if there is a 403 image, display it.
376 $accesspath = file_create_path('imagecache/'. $preset .'.403.png');
377 if (is_file($accesspath)) {
378 imagecache_transfer($accesspath);
379 exit;
380 }
381 header('HTTP/1.0 403 Forbidden');
382 exit;
383 }
384 }
385
386 /**
387 * Handle request validation and responses to ImageCache requests.
388 *
389 * @see imagecache_generate_image() if you're writing code that needs to have
390 * ImageCache generate images but not send them to a browser.
391 */
392 function _imagecache_cache($presetname, $path) {
393 if (!$preset = imagecache_preset_by_name($presetname)) {
394 // Send a 404 if we don't know of a preset.
395 header("HTTP/1.0 404 Not Found");
396 exit;
397 }
398
399 // umm yeah deliver it early if it is there. especially useful
400 // to prevent lock files from being created when delivering private files.
401 $dst = imagecache_create_path($preset['presetname'], $path);
402 if (is_file($dst)) {
403 imagecache_transfer($dst);
404 }
405
406 // preserve path for watchdog.
407 $src = $path;
408
409 // Check if the path to the file exists.
410 if (!is_file($src) && !is_file($src = file_create_path($src))) {
411 watchdog('imagecache', '404: Unable to find %image ', array('%image' => $src), WATCHDOG_ERROR);
412 header("HTTP/1.0 404 Not Found");
413 exit;
414 };
415
416 // Bail if the requested file isn't an image you can't request .php files
417 // etc...
418 if (!getimagesize($src)) {
419 watchdog('imagecache', '403: File is not an image %image ', array('%image' => $src), WATCHDOG_ERROR);
420 header('HTTP/1.0 403 Forbidden');
421 exit;
422 }
423
424 $lockfile = file_directory_temp() .'/'. $preset['presetname'] . basename($src);
425 if (file_exists($lockfile)) {
426 watchdog('imagecache', 'ImageCache already generating: %dst, Lock file: %tmp.', array('%dst' => $dst, '%tmp' => $lockfile), WATCHDOG_NOTICE);
427 // 307 Temporary Redirect, to myself. Lets hope the image is done next time around.
428 header('Location: '. request_uri(), TRUE, 307);
429 exit;
430 }
431 touch($lockfile);
432 // register the shtdown function to clean up lock files. by the time shutdown
433 // functions are being called the cwd has changed from document root, to
434 // server root so absolute paths must be used for files in shutdown functions.
435 register_shutdown_function('file_delete', realpath($lockfile));
436
437 // check if deriv exists... (file was created between apaches request handler and reaching this code)
438 // otherwise try to create the derivative.
439 if (file_exists($dst) || imagecache_build_derivative($preset['actions'], $src, $dst)) {
440 imagecache_transfer($dst);
441 }
442 // Generate an error if image could not generate.
443 watchdog('imagecache', 'Failed generating an image from %image using imagecache preset %preset.', array('%image' => $path, '%preset' => $preset['presetname']), WATCHDOG_ERROR);
444 header("HTTP/1.0 500 Internal Server Error");
445 exit;
446 }
447
448 /**
449 * Apply an action to an image.
450 *
451 * @param $action
452 * Action array
453 * @param $image
454 * Image object
455 * @return
456 * Boolean, TRUE indicating success and FALSE failure.
457 */
458 function _imagecache_apply_action($action, $image) {
459 $actions = imagecache_action_definitions();
460
461 if ($definition = imagecache_action_definition($action['action'])) {
462 $function = $action['action'] .'_image';
463 if (function_exists($function)) {
464 return $function($image, $action['data']);
465 }
466 }
467 // skip undefined actions.. module probably got uninstalled or disabled.
468 watchdog('imagecache', 'non-existant action %action', array('%action' => $action['action']), WATCHDOG_NOTICE);
469 return TRUE;
470 }
471
472 /**
473 * Helper function to transfer files from imagecache.
474 *
475 * Determines MIME type and sets a last modified header.
476 *
477 * @param $path
478 * String containing the path to file to be transferred.
479 * @return
480 * This function does not return. It calls exit().
481 */
482
483 function imagecache_transfer($path) {
484 $size = getimagesize($path);
485 $headers = array('Content-Type: '. mime_header_encode($size['mime']));
486
487 if ($fileinfo = stat($path)) {
488 $headers[] = 'Content-Length: '. $fileinfo[7];
489 _imagecache_cache_set_cache_headers($fileinfo, $headers);
490 }
491 file_transfer($path, $headers);
492 exit;
493 }
494
495 /**
496 * Set file headers that handle "If-Modified-Since" correctly for the
497 * given fileinfo.
498 *
499 * Note that this function may return or may call exit().
500 *
501 * Most code has been taken from drupal_page_cache_header().
502 *
503 * @param $fileinfo
504 * Array returned by stat().
505 * @param
506 * Array of existing headers.
507 * @return
508 * Nothing but beware that this function may not return.
509 */
510 function _imagecache_cache_set_cache_headers($fileinfo, &$headers) {
511 // Set default values:
512 $last_modified = gmdate('D, d M Y H:i:s', $fileinfo[9]) .' GMT';
513 $etag = '"'. md5($last_modified) .'"';
514
515 // See if the client has provided the required HTTP headers:
516 $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
517 ? stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE'])
518 : FALSE;
519 $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH'])
520 ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH'])
521 : FALSE;
522
523 if ($if_modified_since && $if_none_match
524 && $if_none_match == $etag // etag must match
525 && $if_modified_since == $last_modified) { // if-modified-since must match
526 header('HTTP/1.1 304 Not Modified');
527 // All 304 responses must send an etag if the 200 response
528 // for the same object contained an etag
529 header('Etag: '. $etag);
530 // We must also set Last-Modified again, so that we overwrite Drupal's
531 // default Last-Modified header with the right one
532 header('Last-Modified: '. $last_modified);
533 exit;
534 }
535
536 // Send appropriate response:
537 $headers[] = 'Last-Modified: '. $last_modified;
538 $headers[] = 'ETag: '. $etag;
539 }
540
541 /**
542 * Create a new image based on an image preset.
543 *
544 * @param $preset
545 * An image preset array.
546 * @param $source
547 * Path of the source file.
548 * @param $destination
549 * Path of the destination file.
550 * @return
551 * TRUE if an image derivative is generated, FALSE if no image
552 * derivative is generated. NULL if the derivative is being generated.
553 */
554 function imagecache_build_derivative($actions, $src, $dst) {
555 // get the folder for the final location of this preset...
556 $dir = dirname($dst);
557
558 // Build the destination folder tree if it doesn't already exists.
559 if (!file_check_directory($dir, FILE_CREATE_DIRECTORY) && !mkdir($dir, 0775, TRUE)) {
560 watchdog('imagecache', 'Failed to create imagecache directory: %dir', array('%dir' => $dir), WATCHDOG_ERROR);
561 return FALSE;
562 }
563
564 // file_check_directory() has an annoying habit of displaying "directory ...
565 // has been created" status messages. To avoid confusing visitors we clear
566 // out all the status messages for non-ImageCache admins. This might affect
567 // some other messages but errors and warnings should still be displayed.
568 if (!user_access('administer imagecache')) {
569 drupal_get_messages('status', TRUE);
570 }
571
572 // Simply copy the file if there are no actions.
573 if (empty($actions)) {
574 return file_copy($src, $dst, FILE_EXISTS_REPLACE);
575 }
576
577 if (!$image = imageapi_image_open($src)) {
578 return FALSE;
579 }
580
581 if (file_exists($dst)) {
582 watchdog('imagecache', 'Cached image file %dst already exists but is being regenerated. There may be an issue with your rewrite configuration.', array('%dst' => $dst), WATCHDOG_WARNING);
583 }
584
585 foreach ($actions as $action) {
586 if (!empty($action['data'])) {
587 // Make sure the width and height are computed first so they can be used
588 // in relative x/yoffsets like 'center' or 'bottom'.
589 if (isset($action['data']['width'])) {
590 $action['data']['width'] = _imagecache_percent_filter($action['data']['width'], $image->info['width']);
591 }
592 if (isset($action['data']['height'])) {
593 $action['data']['height'] = _imagecache_percent_filter($action['data']['height'], $image->info['height']);
594 }
595 if (isset($action['data']['xoffset'])) {
596 $action['data']['xoffset'] = _imagecache_keyword_filter($action['data']['xoffset'], $image->info['width'], $action['data']['width']);
597 }
598 if (isset($action['data']['yoffset'])) {
599 $action['data']['yoffset'] = _imagecache_keyword_filter($action['data']['yoffset'], $image->info['height'], $action['data']['height']);
600 }
601 }
602 if (!_imagecache_apply_action($action, $image)) {
603 watchdog('imagecache', 'action(id:%id): %action failed for %src', array('%id' => $action['actionid'], '%action' => $action['action'], '%src' => $src), WATCHDOG_ERROR);
604 return FALSE;
605 }
606 }
607
608 if (!imageapi_image_close($image, $dst)) {
609 watchdog('imagecache', 'There was an error saving the new image file %dst.', array('%dst' => $dst), WATCHDOG_ERROR);
610 return FALSE;
611 }
612
613 return TRUE;
614 }
615
616 /**
617 * Implementation of hook_user().
618 */
619 function imagecache_user($op, &$edit, &$account, $category = NULL) {
620 // Flush cached old user picture.
621 if ($op == 'update' && !empty($account->picture)) {
622 imagecache_image_flush($account->picture);
623 }
624 }
625
626 /**
627 * Implementation of filefield.module's hook_file_delete().
628 *
629 * Remove derivative images after the originals are deleted by filefield.
630 */
631 function imagecache_file_delete($file) {
632 imagecache_image_flush($file->filepath);
633 }
634
635 /**
636 * Implementation of hook_field_formatter_info().
637 *
638 * imagecache formatters are named as $presetname_$style
639 * $style is used to determine how the preset should be rendered.
640 * If you are implementing custom imagecache formatters please treat _ as
641 * reserved.
642 *
643 * @todo: move the linking functionality up to imagefield and clean up the default image
644 * integration.
645 */
646 function imagecache_field_formatter_info() {
647 $formatters = array();
648 foreach (imagecache_presets() as $preset) {
649 $formatters[$preset['presetname'] .'_default'] = array(
650 'label' => t('@preset image', array('@preset' => $preset['presetname'])),
651 'field types' => array('image', 'filefield'),
652 );
653 $formatters[$preset['presetname'] .'_linked'] = array(
654 'label' => t('@preset image linked to node', array('@preset' => $preset['presetname'])),
655 'field types' => array('image', 'filefield'),
656 );
657 $formatters[$preset['presetname'] .'_imagelink'] = array(
658 'label' => t('@preset image linked to image', array('@preset' => $preset['presetname'])),
659 'field types' => array('image', 'filefield'),
660 );
661 $formatters[$preset['presetname'] .'_path'] = array(
662 'label' => t('@preset file path', array('@preset' => $preset['presetname'])),
663 'field types' => array('image', 'filefield'),
664 );
665 $formatters[$preset['presetname'] .'_url'] = array(
666 'label' => t('@preset URL', array('@preset' => $preset['presetname'])),
667 'field types' => array('image', 'filefield'),
668 );
669 }
670 return $formatters;
671 }
672
673 function theme_imagecache_formatter_default($element) {
674 // Inside a view $element may contain NULL data. In that case, just return.
675 if (empty($element['#item']['fid'])) {
676 return '';
677 }
678
679 // Extract the preset name from the formatter name.
680 $presetname = substr($element['#formatter'], 0, strrpos($element['#formatter'], '_'));
681 $style = 'linked';
682 $style = 'default';
683
684 $item = $element['#item'];
685 $item['data']['alt'] = isset($item['data']['alt']) ? $item['data']['alt'] : '';
686 $item['data']['title'] = isset($item['data']['title']) ? $item['data']['title'] : NULL;
687
688 $class = "imagecache imagecache-$presetname imagecache-$style imagecache-{$element['#formatter']}";
689 return theme('imagecache', $presetname, $item['filepath'], $item['data']['alt'], $item['data']['title'], array('class' => $class));
690 }
691
692 function theme_imagecache_formatter_linked($element) {
693 // Inside a view $element may contain NULL data. In that case, just return.
694 if (empty($element['#item']['fid'])) {
695 return '';
696 }
697
698 // Extract the preset name from the formatter name.
699 $presetname = substr($element['#formatter'], 0, strrpos($element['#formatter'], '_'));
700 $style = 'linked';
701
702 $item = $element['#item'];
703 $item['data']['alt'] = isset($item['data']['alt']) ? $item['data']['alt'] : '';
704 $item['data']['title'] = isset($item['data']['title']) ? $item['data']['title'] : NULL;
705
706 $imagetag = theme('imagecache', $presetname, $item['filepath'], $item['data']['alt'], $item['data']['title']);
707 $path = empty($item['nid']) ? '' : 'node/'. $item['nid'];
708 $class = "imagecache imagecache-$presetname imagecache-$style imagecache-{$element['#formatter']}";
709 return l($imagetag, $path, array('attributes' => array('class' => $class), 'html' => TRUE));
710 }
711
712 function theme_imagecache_formatter_imagelink($element) {
713 // Inside a view $element may contain NULL data. In that case, just return.
714 if (empty($element['#item']['fid'])) {
715 return '';
716 }
717
718 // Extract the preset name from the formatter name.
719 $presetname = substr($element['#formatter'], 0, strrpos($element['#formatter'], '_'));
720 $style = 'imagelink';
721
722 $item = $element['#item'];
723 $item['data']['alt'] = isset($item['data']['alt']) ? $item['data']['alt'] : '';
724 $item['data']['title'] = isset($item['data']['title']) ? $item['data']['title'] : NULL;
725
726 $imagetag = theme('imagecache', $presetname, $item['filepath'], $item['data']['alt'], $item['data']['title']);
727 $path = file_create_url($item['filepath']);
728 $class = "imagecache imagecache-$presetname imagecache-$style imagecache-{$element['#formatter']}";
729 return l($imagetag, $path, array('attributes' => array('class' => $class), 'html' => TRUE));
730 }
731
732 function theme_imagecache_formatter_path($element) {
733 // Inside a view $element may contain NULL data. In that case, just return.
734 if (empty($element['#item']['fid'])) {
735 return '';
736 }
737
738 // Extract the preset name from the formatter name.
739 $presetname = substr($element['#formatter'], 0, strrpos($element['#formatter'], '_'));
740
741 return imagecache_create_path($presetname, $element['#item']['filepath']);
742 }
743
744 function theme_imagecache_formatter_url($element) {
745 // Inside a view $element may contain NULL data. In that case, just return.
746 if (empty($element['#item']['fid'])) {
747 return '';
748 }
749
750 // Extract the preset name from the formatter name.
751 $presetname = substr($element['#formatter'], 0, strrpos($element['#formatter'], '_'));
752
753 return imagecache_create_url($presetname, $element['#item']['filepath']);
754 }
755
756 /**
757 * Accept a percentage and return it in pixels.
758 */
759 function _imagecache_percent_filter($value, $current_pixels) {
760 if (strpos($value, '%') !== FALSE) {
761 $value = str_replace('%', '', $value) * 0.01 * $current_pixels;
762 }
763 return $value;
764 }
765
766 /**
767 * Accept a keyword (center, top, left, etc) and return it as an offset in pixels.
768 */
769 function _imagecache_keyword_filter($value, $current_pixels, $new_pixels) {
770 switch ($value) {
771 case 'top':
772 case 'left':
773 $value = 0;
774 break;
775 case 'bottom':
776 case 'right':
777 $value = $current_pixels - $new_pixels;
778 break;
779 case 'center':
780 $value = $current_pixels/2 - $new_pixels/2;
781 break;
782 }
783 return $value;
784 }
785
786 /**
787 * Recursively delete all files and folders in the specified filepath, then
788 * delete the containing folder.
789 *
790 * Note that this only deletes visible files with write permission.
791 *
792 * @param string $path
793 * A filepath relative to file_directory_path.
794 */
795 function _imagecache_recursive_delete($path) {
796 if (is_file($path) || is_link($path)) {
797 unlink($path);
798 }
799 elseif (is_dir($path)) {
800 $d = dir($path);
801 while (($entry = $d->read()) !== FALSE) {
802 if ($entry == '.' || $entry == '..') continue;
803 $entry_path = $path .'/'. $entry;
804 _imagecache_recursive_delete($entry_path);
805 }
806 $d->close();
807 rmdir($path);
808 }
809 else {
810 watchdog('imagecache', 'Unknown file type(%path) stat: %stat ',
811 array('%path' => $path, '%stat' => print_r(stat($path),1)), WATCHDOG_ERROR);
812 }
813
814 }
815
816 /**
817 * Create and image tag for an imagecache derivative
818 *
819 * @param $presetname
820 * String with the name of the preset used to generate the derivative image.
821 * @param $path
822 * String path to the original image you wish to create a derivative image
823 * tag for.
824 * @param $alt
825 * Optional string with alternate text for the img element.
826 * @param $title
827 * Optional string with title for the img element.
828 * @param $attributes
829 * Optional drupal_attributes() array. If $attributes is an array then the
830 * default imagecache classes will not be set automatically, you must do this
831 * manually.
832 * @param $getsize
833 * If set to TRUE, the image's dimension are fetched and added as width/height
834 * attributes.
835 * @param $absolute
836 * A Boolean indicating that the URL should be absolute. Defaults to TRUE.
837 * @return
838 * HTML img element string.
839 */
840 function theme_imagecache($presetname, $path, $alt = '', $title = '', $attributes = NULL, $getsize = TRUE, $absolute = TRUE) {
841 // Check is_null() so people can intentionally pass an empty array of
842 // to override the defaults completely.
843 if (is_null($attributes)) {
844 $attributes = array('class' => 'imagecache imagecache-'. $presetname);
845 }
846 if ($getsize && ($image = image_get_info(imagecache_create_path($presetname, $path)))) {
847 $attributes['width'] = $image['width'];
848 $attributes['height'] = $image['height'];
849 }
850
851 $attributes = drupal_attributes($attributes);
852 $imagecache_url = imagecache_create_url($presetname, $path, FALSE, $absolute);
853 return '<img src="'. $imagecache_url .'" alt="'. check_plain($alt) .'" title="'. check_plain($title) .'" '. $attributes .' />';
854 }
855
856 /**
857 * Create a link the original image that wraps the derivative image.
858 *
859 * @param $presetname
860 * String with the name of the preset used to generate the derivative image.
861 * @param $path
862 * String path to the original image you wish to create a derivative image
863 * tag for.
864 * @param $alt
865 * Optional string with alternate text for the img element.
866 * @param $title
867 * Optional string with title for the img element.
868 * @param attributes
869 * Optional drupal_attributes() array for the link.
870 * @return
871 * An HTML string.
872 */
873 function theme_imagecache_imagelink($presetname, $path, $alt = '', $title = '', $attributes = NULL) {
874 $image = theme('imagecache', $presetname, $path, $alt, $title);
875 $original_image_url = file_create_url($path);
876 return l($image, $original_image_url, array('absolute' => FALSE, 'html' => TRUE, 'attributes' => $attributes));
877 }
878
879 /**
880 * ImageCache 2.x API
881 *
882 * The API for imagecache has changed. The 2.x API returns more structured
883 * data, has shorter function names, and implements more aggressive metadata
884 * caching.
885 *
886 */
887
888 /**
889 * Get an array of all presets and their settings.
890 *
891 * @param reset
892 * if set to TRUE it will clear the preset cache
893 *
894 * @return
895 * array of presets array( $preset_id => array('presetid' => integer, 'presetname' => string))
896 */
897 function imagecache_presets($reset = FALSE) {
898 static $presets = array();
899
900 // Clear caches if $reset is TRUE;
901 if ($reset) {
902 $presets = array();
903 cache_clear_all('imagecache:presets', 'cache');
904
905 // Clear the content.module cache (refreshes the list of formatters provided by imagefield.module).
906 if (module_exists('content')) {
907 content_clear_type_cache();
908 }
909 }
910 // Return presets if the array is populated.
911 if (!empty($presets)) {
912 return $presets;
913 }
914
915 // Grab from cache or build the array. To ensure that the Drupal 5 upgrade
916 // path works, we also check whether the presets list is an array.
917 if (($cache = cache_get('imagecache:presets', 'cache')) && is_array($cache->data)) {
918 $presets = $cache->data;
919 }
920 else {
921 $normal_presets = array();
922
923 $result = db_query('SELECT * FROM {imagecache_preset} ORDER BY presetname');
924 while ($preset = db_fetch_array($result)) {
925 $presets[$preset['presetid']] = $preset;
926 $presets[$preset['presetid']]['actions'] = imagecache_preset_actions($preset);
927 $presets[$preset['presetid']]['storage'] = IMAGECACHE_STORAGE_NORMAL;
928
929 // Collect normal preset names so we can skip defaults and mark overrides accordingly
930 $normal_presets[$preset['presetname']] = $preset['presetid'];
931 }
932
933 // Collect default presets and allow modules to modify them before they
934 // are cached.
935 $default_presets = module_invoke_all('imagecache_default_presets');
936 drupal_alter('imagecache_default_presets', $default_presets);
937
938 // Add in default presets if they don't conflict with any normal presets.
939 // Mark normal presets that take the same preset namespace as overrides.
940 foreach ($default_presets as $preset) {
941 if (!empty($preset['presetname'])) {
942 if (!isset($normal_presets[$preset['presetname']])) {
943 $preset['storage'] = IMAGECACHE_STORAGE_DEFAULT;
944 // Use a string preset identifier
945 $preset['presetid'] = $preset['presetname'];
946 $presets[$preset['presetname']] = $preset;
947 }
948 else {
949 $presetid = $normal_presets[$preset['presetname']];
950 $presets[$presetid]['storage'] = IMAGECACHE_STORAGE_OVERRIDE;
951 }
952 }
953 }
954
955 cache_set('imagecache:presets', $presets);
956 }
957 return $presets;
958 }
959
960 /**
961 * Load a preset by preset_id.
962 *
963 * @param preset_id
964 * The numeric id of a preset.
965 *
966 * @return
967 * preset array( 'presetname' => string, 'presetid' => integet)
968 * empty array if preset_id is an invalid preset
969 */
970 function imagecache_preset($preset_id, $reset = FALSE) {
971 $presets = imagecache_presets($reset);
972 return (isset($presets[$preset_id])) ? $presets[$preset_id] : array();
973 }
974
975 /**
976 * Load a preset by name.
977 *
978 * @param preset_name
979 *
980 * @return
981 * preset array( 'presetname' => string, 'presetid' => integer)
982 * empty array if preset_name is an invalid preset
983 */
984
985 function imagecache_preset_by_name($preset_name) {
986 static $presets_by_name = array();
987 if (!$presets_by_name && $presets = imagecache_presets()) {
988 foreach ($presets as $preset) {
989 $presets_by_name[$preset['presetname']] = $preset;
990 }
991 }
992 return (isset($presets_by_name[$preset_name])) ? $presets_by_name[$preset_name] : array();
993 }
994
995 /**
996 * Save an ImageCache preset.
997 *
998 * @param preset
999 * an imagecache preset array.
1000 * @return
1001 * a preset array. In the case of a new preset, 'presetid' will be populated.
1002 */
1003 function imagecache_preset_save($preset) {
1004 // @todo: CRUD level validation?
1005 if (isset($preset['presetid']) && is_numeric($preset['presetid'])) {
1006 drupal_write_record('imagecache_preset', $preset, 'presetid');
1007 }
1008 else {
1009 drupal_write_record('imagecache_preset', $preset);
1010 }
1011
1012 // Reset presets cache.
1013 imagecache_preset_flush($preset);
1014 imagecache_presets(TRUE);
1015
1016 // Rebuild Theme Registry
1017 drupal_rebuild_theme_registry();
1018
1019 return $preset;
1020 }
1021
1022 function imagecache_preset_delete($preset) {
1023 imagecache_preset_flush($preset['presetid']);
1024 db_query('DELETE FROM {imagecache_action} where presetid = %d', $preset['presetid']);
1025 db_query('DELETE FROM {imagecache_preset} where presetid = %d', $preset['presetid']);
1026 imagecache_presets(TRUE);
1027 return TRUE;
1028 }
1029
1030 function imagecache_preset_actions($preset, $reset = FALSE) {
1031 static $actions_cache = array();
1032
1033 if ($reset || empty($actions_cache[$preset['presetid']])) {
1034 $result = db_query('SELECT * FROM {imagecache_action} where presetid = %d order by weight', $preset['presetid']);
1035 while ($row = db_fetch_array($result)) {
1036 $row['data'] = unserialize($row['data']);
1037 $actions_cache[$preset['presetid']][] = $row;
1038 }
1039 }
1040
1041 return isset($actions_cache[$preset['presetid']]) ? $actions_cache[$preset['presetid']] : array();
1042 }
1043
1044 /**
1045 * Flush cached media for a preset.
1046 * @param id
1047 * A preset id.
1048 */
1049 function imagecache_preset_flush($preset) {
1050 if (user_access('flush imagecache')) {
1051 $presetdir = realpath(file_directory_path() .'/imagecache/'. $preset['presetname']);
1052 if (is_dir($presetdir)) {
1053 _imagecache_recursive_delete($presetdir);
1054 }
1055 }
1056 }
1057
1058 /**
1059 * Clear cached versions of a specific file in all presets.
1060 * @param $path
1061 * The Drupal file path to the original image.
1062 */
1063 function imagecache_image_flush($path) {
1064 foreach (imagecache_presets() as $preset) {
1065 file_delete(imagecache_create_path($preset['presetname'], $path));
1066 }
1067 }
1068
1069 function imagecache_action($actionid) {
1070 static $actions;
1071
1072 if (!isset($actions[$actionid])) {
1073 $action = array();
1074
1075 $result = db_query('SELECT * FROM {imagecache_action} WHERE actionid=%d', $actionid);
1076 if ($row = db_fetch_array($result)) {
1077 $action = $row;
1078 $action['data'] = unserialize($action['data']);
1079
1080 $definition = imagecache_action_definition($action['action']);
1081 $action = array_merge($definition, $action);
1082 $actions[$actionid] = $action;
1083 }
1084 }
1085 return $actions[$actionid];
1086 }
1087
1088 function imagecache_action_load($actionid) {
1089 return imagecache_action($actionid, TRUE);
1090 }
1091
1092 function imagecache_action_save($action) {
1093 $definition = imagecache_action_definition($action['action']);
1094 $action = array_merge($definition, $action);
1095
1096 if (!empty($action['actionid'])) {
1097 drupal_write_record('imagecache_action', $action, 'actionid');
1098 }
1099 else {
1100 drupal_write_record('imagecache_action', $action);
1101 }
1102 $preset = imagecache_preset($action['presetid']);
1103 imagecache_preset_flush($preset);
1104 imagecache_presets(TRUE);
1105 return $action;
1106 }
1107
1108 function imagecache_action_delete($action) {
1109 db_query('DELETE FROM {imagecache_action} WHERE actionid=%d', $action['actionid']);
1110 $preset = imagecache_preset($action['presetid']);
1111 imagecache_preset_flush($preset);
1112 imagecache_presets(TRUE);
1113 }
1114
1115 /**
1116 * Implementation of hook_action_info().
1117 *
1118 * Note: These are actions in the Drupal core trigger.module sense, not
1119 * ImageCache actions.
1120 */
1121 function imagecache_action_info() {
1122 $actions = array();
1123
1124 if (module_exists('filefield')) {
1125 $actions['imagecache_flush_action'] = array(
1126 'type' => 'node',
1127 'description' => t("ImageCache: Flush ALL presets for this node's filefield images"),
1128 'configurable' => FALSE,
1129 'hooks' => array(
1130 'nodeapi' => array('presave', 'delete', 'insert', 'update'),
1131 )
1132 );
1133 $actions['imagecache_generate_all_action'] = array(
1134 'type' => 'node',
1135 'description' => t("ImageCache: Generate ALL presets for this node's filefield images"),
1136 'configurable' => FALSE,
1137 'hooks' => array(
1138 'nodeapi' => array('presave', 'insert', 'update'),
1139 )
1140 );
1141 $actions['imagecache_generate_action'] = array(
1142 'type' => 'node',
1143 'description' => t("ImageCache: Generate configured preset(s) for this node's filefield images"),
1144 'configurable' => TRUE,
1145 'hooks' => array(
1146 'nodeapi' => array('presave', 'insert', 'update'),
1147 )
1148 );
1149 }
1150
1151 return $actions;
1152 }
1153
1154 /**
1155 * Flush all imagecache presets for a given node.
1156 *
1157 * @param $node
1158 * A node object.
1159 * @param $context
1160 * Contains values from the calling action.
1161 *
1162 * @see imagecache_action_info()
1163 */
1164 function imagecache_flush_action(&$node, $context) {
1165 $files = imagecache_get_images_in_node($node);
1166 if (!empty($files)) {
1167 foreach ($files as $file) {
1168 imagecache_image_flush($file['filepath']);
1169 }
1170 }
1171 }
1172
1173 /**
1174 * Generate all imagecache presets for the given node.
1175 *
1176 * @param $node
1177 * A node object.
1178 * @param $context
1179 * Contains values from the calling action.
1180 *
1181 * @see imagecache_action_info()
1182 */
1183 function imagecache_generate_all_action(&$node, $context) {
1184 $files = imagecache_get_images_in_node($node);
1185 $presets = imagecache_presets();
1186 if (!empty($files) && !empty($presets)) {
1187 foreach ($files as $file) {
1188 foreach ($presets as $presetname) {
1189 imagecache_generate_image($presetname['presetname'], $file['filepath']);
1190 }
1191 }
1192 }
1193 }
1194
1195 /**
1196 * Generate imagecache presets for the given node and presets.
1197 *
1198 * @param $node
1199 * A node object.
1200 * @param $context
1201 * Contains values from the calling action.
1202 *
1203 * @see imagecache_action_info()
1204 * @see imagecache_generate_action_form()
1205 */
1206 function imagecache_generate_action(&$node, $context) {
1207 $files = imagecache_get_images_in_node($node);
1208 if (!empty($files) && !empty($context['imagecache_presets'])) {
1209 foreach ($files as $file) {
1210 foreach ($context['imagecache_presets'] as $presetname) {
1211 imagecache_generate_image($presetname, $file['filepath']);
1212 }
1213 }
1214 }
1215 }
1216
1217 /**
1218 * Form for configuring the generate action.
1219 *
1220 * @see imagecache_generate_action()
1221 */
1222 function imagecache_generate_action_form($context) {
1223 $options = array();
1224 foreach (imagecache_presets() as $preset) {
1225 $options[$preset['presetname']] = $preset['presetname'];
1226 }
1227 $form['presets'] = array(
1228 '#type' => 'checkboxes',
1229 '#options' => $options,
1230 '#description' => t('Select which imagecache presets will be effected'),
1231 '#required' => TRUE,
1232 '#default_value' => isset($context['imagecache_presets']) ? $context['imagecache_presets'] : array(),
1233 );
1234 // Filter out false checkboxes: http://drupal.org/node/61760#comment-402631
1235 $form['array_filter'] = array('#type' => 'value', '#value' => TRUE);
1236 return $form;
1237 }
1238
1239 /**
1240 * Generate a derivative image given presetname and filepath.
1241 *
1242 * This is a developer friendly version of _imagecache_cache(), it doesn't worry
1243 * about sending HTTP headers or an image back to the client so it's much
1244 * simpler.
1245 *
1246 * @param $presetname
1247 * ImageCache preset array.
1248 * @param $filepath
1249 * String filepath from the files table.
1250 * @return
1251 * A Boolean indicating if the operation succeeded.
1252 */
1253 function imagecache_generate_image($presetname, $filepath) {
1254 $preset = imagecache_preset_by_name($presetname);
1255 if (empty($preset['presetname'])) {
1256 return FALSE;
1257 }
1258 $destination = imagecache_create_path($preset['presetname'], $filepath);
1259 if (file_exists($destination)) {
1260 return TRUE;
1261 }
1262 return imagecache_build_derivative($preset['actions'], $filepath, $destination);
1263 }
1264
1265 /**
1266 * Given a node, get all images associated with it.
1267 *
1268 * Currently this only works with images stored in filefields.
1269 *
1270 * @param $node
1271 * Node object.
1272 * @return
1273 * An array of info from the files table.
1274 */
1275 function imagecache_get_images_in_node(&$node) {
1276 $files = array();
1277 if (module_exists('filefield')) {
1278 $data = filefield_get_node_files($node);
1279 foreach ($data as $key => $value) {
1280 if (stristr($value['filemime'], 'image')) {
1281 $files[$key] = $value;
1282 }
1283 }
1284 }
1285 return $files;
1286 }