#255421 by yhahn: Import/export ImageCache presets.
[project/imagecache.git] / imagecache.module
CommitLineData
65d53f72 1<?php
e0cb144b 2// $Id$
65d53f72 3
1c784f3b
DP
4/**
5 * @file
6 * Dynamic image resizer and image cacher.
7 *
e1bc2b25 8 * ImageCache allows you to setup presets for image processing.
23c2a416 9 * If an ImageCache derivative doesn't exist the web server's
a00e85b2
DP
10 * rewrite rules will pass the request to Drupal which in turn
11 * hands it off to imagecache to dynamically generate the file.
23c2a416 12 *
13 * To view a derivative image you request a special url containing
a00e85b2
DP
14 * 'imagecache/<presetname>/path/to/file.ext.
15 *
16 * Presets can be managed at http://example.com/admin/build/imagecache.
17 *
23c2a416 18 * To view a derivative image you request a special url containing
a00e85b2
DP
19 * 'imagecache/<presetname>/path/to/file.ext.
20 *
21 * If you had a preset names 'thumbnail' and you wanted to see the
23c2a416 22 * thumbnail version of http://example.com/files/path/to/myimage.jpg you
a00e85b2
DP
23 * would use http://example.com/files/imagecache/thumbnail/path/to/myimage.jpg
24 *
23c2a416 25 * ImageCache provides formatters for CCK Imagefields and is leveraged by several
a00e85b2
DP
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.
23c2a416 28 *
1c784f3b 29 * @todo: add watermarking capabilities.
23c2a416 30 *
1c784f3b 31 */
65d53f72 32
6624dc14 33/**
34 * Imagecache preset storage constant for user-defined presets in the DB.
35 */
36define('IMAGECACHE_STORAGE_NORMAL', 0);
37
38/**
39 * Imagecache preset storage constant for module-defined presets in code.
40 */
41define('IMAGECACHE_STORAGE_DEFAULT', 1);
42
43/**
44 * Imagecache preset storage constant for user-defined presets that override
45 * module-defined presets.
46 */
47define('IMAGECACHE_STORAGE_OVERRIDE', 2);
42cc9535
DP
48
49/*********************************************************************************************
50 * Drupal Hooks
51 *********************************************************************************************/
52
1c784f3b
DP
53/**
54 * Implementation of hook_perm().
55 */
e0cb144b 56function imagecache_perm() {
42669fda 57 $perms = array('administer imagecache', 'flush imagecache');
65312e36 58 foreach (imagecache_presets() as $preset) {
42cc9535 59 $perms[] = 'view imagecache '. $preset['presetname'];
42669fda
DP
60 }
61 return $perms;
e0cb144b
DP
62}
63
1c784f3b
DP
64/**
65 * Implementation of hook_menu().
66 */
8c361d7c 67function imagecache_menu() {
65d53f72 68 $items = array();
42669fda 69
8c361d7c
DP
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
65d53f72
DP
83 return $items;
84}
85
6624dc14 86/**
87 * Clear imagecache presets cache on admin/build/modules form.
88 */
89function imagecache_form_system_modules_alter(&$form, $form_state) {
90 imagecache_presets(TRUE);
91}
8c361d7c
DP
92
93/**
94 * Implementation of hook_theme().
95 */
96function 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 'arguments' => array('element' => NULL),
115 ),
116 'imagecache_scale' => array(
117 'file' => 'imagecache_actions.inc',
118 'arguments' => array('element' => NULL),
119 ),
120 'imagecache_scale_and_crop' => array(
121 'file' => 'imagecache_actions.inc',
122 'arguments' => array('element' => NULL),
123 ),
124 'imagecache_deprecated_scale' => array(
125 'file' => 'imagecache_actions.inc',
126 'arguments' => array('element' => NULL),
127 ),
128 'imagecache_crop' => array(
129 'file' => 'imagecache_actions.inc',
130 'arguments' => array('element' => NULL),
131 ),
132 'imagecache_desaturate' => array(
133 'file' => 'imagecache_actions.inc',
134 'arguments' => array('element' => NULL),
135 ),
136 'imagecache_rotate' => array(
137 'file' => 'imagecache_actions.inc',
138 'arguments' => array('element' => NULL),
139 ),
2684c3e3
DP
140 'imagecache_sharpen' => array(
141 'file' => 'imagecache_actions.inc',
142 'arguments' => array('element' => NULL),
143 ),
8c361d7c
DP
144 );
145
146 foreach (imagecache_presets() as $preset) {
147 $theme['imagecache_formatter_'. $preset['presetname'] .'_default'] = array(
148 'arguments' => array('element' => NULL),
149 'function' => 'theme_imagecache_formatter',
150 );
151 $theme['imagecache_formatter_'. $preset['presetname'] .'_linked'] = array(
152 'arguments' => array('element' => NULL),
153 'function' => 'theme_imagecache_formatter',
154 );
155 $theme['imagecache_formatter_'. $preset['presetname'] .'_imagelink'] = array(
156 'arguments' => array('element' => NULL),
157 'function' => 'theme_imagecache_formatter',
158 );
159 $theme['imagecache_formatter_'. $preset['presetname'] .'_path'] = array(
160 'arguments' => array('element' => NULL),
161 'function' => 'theme_imagecache_formatter',
162 );
163 $theme['imagecache_formatter_'. $preset['presetname'] .'_url'] = array(
164 'arguments' => array('element' => NULL),
165 'function' => 'theme_imagecache_formatter',
166 );
167 }
168
169 return $theme;
170
171}
172
b7c5115b 173/**
650d9de9
DP
174 * Implementation of hook_imagecache_actions.
175 *
176 * @return array
8c361d7c
DP
177 * An array of information on the actions implemented by a module. The array
178 * contains a sub-array for each action node type, with the machine-readable
23c2a416 179 * action name as the key. Each sub-array has up to 3 attributes. Possible
8c361d7c
DP
180 * attributes:
181 *
650d9de9
DP
182 * "name": the human-readable name of the action. Required.
183 * "description": a brief description of the action. Required.
8c361d7c 184 * "file": the name of the include file the action can be found
650d9de9
DP
185 * in relative to the implementing module's path.
186 */
187function imagecache_imagecache_actions() {
188 $actions = array(
189 'imagecache_resize' => array(
190 'name' => 'Resize',
191 'description' => 'Resize an image to an exact set of dimensions, ignoring aspect ratio.',
192 ),
193 'imagecache_scale' => array(
194 'name' => 'Scale',
195 'description' => 'Resize an image maintaining the original aspect-ratio (only one value necessary).',
196 'file' => 'imagecache_actions.inc',
197 ),
198 'imagecache_deprecated_scale' => array(
199 'name' => 'Deprecated Scale',
e1bc2b25 200 'description' => 'Precursor to Scale and Crop. Has inside and outside dimension support. This action will be removed in ImageCache 2.1).',
650d9de9
DP
201 'file' => 'imagecache_actions.inc',
202 ),
203 'imagecache_scale_and_crop' => array(
204 'name' => 'Scale And Crop',
205 'description' => 'Resize an image to an exact set of dimensions, ignoring aspect ratio.',
206 'file' => 'imagecache_actions.inc',
207 ),
208 'imagecache_crop' => array(
209 'name' => 'Crop',
210 'description' => 'Crop an image to the rectangle specified by the given offsets and dimensions.',
211 'file' => 'imagecache_actions.inc',
212 ),
f3ede8cc
DP
213 'imagecache_desaturate' => array(
214 'name' => 'Desaturate',
215 'description' => 'Convert an image to grey scale.',
216 'file' => 'imagecache_actions.inc',
217 ),
19fefb49
DP
218 'imagecache_rotate' => array(
219 'name' => 'Rotate',
220 'description' => 'Rotate an image.',
221 'file' => 'imagecache_actions.inc',
222 ),
2684c3e3
DP
223 'imagecache_sharpen' => array(
224 'name' => 'Sharpen',
225 'description' => 'Sharpen an image using unsharp masking.',
226 'file' => 'imagecache_actions.inc',
227 ),
650d9de9
DP
228 );
229
650d9de9
DP
230 return $actions;
231}
232
233/**
234 * Pull in actions exposed by other modules using hook_imagecache_actions().
235 *
236 * @param $reset
237 * Boolean flag indicating whether the cached data should be
238 * wiped and recalculated.
239 *
240 * @return
241 * An array of actions to be used when transforming images.
242 */
67ee60c4 243function imagecache_action_definitions($reset = false) {
650d9de9
DP
244 static $actions;
245 if (!isset($actions) || $reset) {
246 if (!$reset && ($cache = cache_get('imagecache_actions')) && !empty($cache->data)) {
8c361d7c 247 $actions = $cache->data;
650d9de9
DP
248 }
249 else {
65312e36 250 foreach (module_implements('imagecache_actions') as $module) {
650d9de9
DP
251 foreach (module_invoke($module, 'imagecache_actions') as $key => $action) {
252 $action['module'] = $module;
8c361d7c 253 if (!empty($action['file'])) {
650d9de9
DP
254 $action['file'] = drupal_get_path('module', $action['module']) .'/'. $action['file'];
255 }
256 $actions[$key] = $action;
257 };
258 }
42cc9535 259 uasort($actions, '_imagecache_definitions_sort');
8c361d7c 260 cache_set('imagecache_actions', $actions);
650d9de9
DP
261 }
262 }
263 return $actions;
264}
265
42cc9535
DP
266function _imagecache_definitions_sort($a, $b) {
267 $a = $a['name'];
268 $b = $b['name'];
269 if ($a == $b) {
270 return 0;
271 }
272 return ($a < $b) ? -1 : 1;
273}
1ae0e73d
DP
274
275function imagecache_action_definition($action) {
276 static $definition_cache;
277 if (!isset($definition_cache[$action])) {
278 $definitions = imagecache_action_definitions();
279 $definition = (isset($definitions[$action])) ? $definitions[$action] : array();
23c2a416 280
349d8430 281 if (isset($definition['file'])) {
1ae0e73d
DP
282 require_once($definition['file']);
283 }
284 $definition_cache[$action] = $definition;
285 }
286 return $definition_cache[$action];
23c2a416 287}
650d9de9
DP
288
289/**
b7c5115b
DP
290 * Return a URL that points to the location of a derivative of the
291 * original image at @p $path, transformed with the given @p $preset.
03321cf3
DP
292 *
293 * Special care is taken to make this work with the possible combinations of
294 * Clean URLs and public/private downloads. For example, when Clean URLs are not
295 * available an URL with query should be returned, like
296 * http://example.com/?q=files/imagecache/foo.jpg, so that imagecache is able
297 * intercept the request for this file.
298 *
299 * This code is very similar to the Drupal core function file_create_url(), but
300 * handles the case of Clean URLs and public downloads differently however.
b7c5115b 301 */
42cc9535 302function imagecache_create_url($presetname, $path) {
b7c5115b 303 $path = _imagecache_strip_file_directory($path);
8c361d7c 304 return file_create_url(file_directory_path() .'/imagecache/'. $presetname .'/'. $path);
b7c5115b
DP
305}
306
307/**
308 * Return a file system location that points to the location of a derivative
309 * of the original image at @p $path, transformed with the given @p $preset.
310 * Keep in mind that the image might not yet exist and won't be created.
311 */
42cc9535 312function imagecache_create_path($presetname, $path) {
b7c5115b 313 $path = _imagecache_strip_file_directory($path);
42cc9535 314 return file_create_path() .'/imagecache/'. $presetname .'/'. $path;
b7c5115b
DP
315}
316
317/**
318 * Remove a possible leading file directory path from the given path.
319 */
320function _imagecache_strip_file_directory($path) {
321 $dirpath = file_directory_path();
322 $dirlen = strlen($dirpath);
323 if (substr($path, 0, $dirlen + 1) == $dirpath .'/') {
324 $path = substr($path, $dirlen + 1);
325 }
326 return $path;
327}
328
329
8c361d7c 330/**
42669fda
DP
331 * callback for handling public files imagecache requests.
332 */
65d53f72 333function imagecache_cache() {
65d53f72 334 $args = func_get_args();
42669fda
DP
335 $preset = check_plain(array_shift($args));
336 $path = implode('/', $args);
337 _imagecache_cache($preset, $path);
338}
277b7f55 339
42669fda
DP
340/**
341 * callback for handling private files imagecache requests
342 */
343function imagecache_cache_private() {
344 $args = func_get_args();
345 $preset = check_plain(array_shift($args));
346 $source = implode('/', $args);
347
348 if (user_access('view imagecache '. $preset)) {
349 _imagecache_cache($preset, $source);
8c361d7c 350 }
42669fda 351 else {
6491b9c4 352 // if there is a 403 image, display it.
42669fda 353 $accesspath = file_create_path('imagecache/'. $preset .'.403.png');
cf662f2d 354 if (is_file($accesspath)) {
42669fda
DP
355 imagecache_transfer($accesspath);
356 exit;
357 }
358 header('HTTP/1.0 403 Forbidden');
82a7e4a6
DP
359 exit;
360 }
42669fda
DP
361}
362
363/**
8c361d7c 364 * handle request validation and responses to imagecache requests.
42669fda 365 */
42cc9535
DP
366function _imagecache_cache($presetname, $path) {
367 if (!$preset = imagecache_preset_by_name($presetname)) {
8c361d7c
DP
368 // Send a 404 if we don't know of a preset.
369 header("HTTP/1.0 404 Not Found");
42669fda
DP
370 exit;
371 }
db5b9a50 372
7d4d7d69 373 // umm yeah deliver it early if it is there. especially useful
8c361d7c 374 // to prevent lock files from being created when delivering private files.
db5b9a50 375 $dst = imagecache_create_path($preset['presetname'], $path);
cf662f2d 376 if (is_file($dst)) {
db5b9a50 377 imagecache_transfer($dst);
8c361d7c
DP
378 }
379
cf662f2d 380 // preserve path for watchdog.
cf8f9fe4 381 $src = $path;
23c2a416 382
cf8f9fe4 383 // check if the path to the file exists
cf662f2d
DP
384 if (!is_file($src) && !is_file($src = file_create_path($src))) {
385 // Send a 404 if we don't know of a preset.
386 header("HTTP/1.0 404 Not Found");
42669fda 387 exit;
cf662f2d 388 };
3edf6c97 389
cf8f9fe4
DP
390 // bail if the requested file isn't an image you can't just summon .php files etc...
391 if (!getimagesize($src)) {
392 header('HTTP/1.0 403 Forbidden');
393 exit;
394 }
395
7d06010f 396 $lockfile = file_directory_temp() .'/'. $preset['presetname'] . basename($src);
44eb6ee1 397 if (file_exists($lockfile)) {
e1bc2b25 398 watchdog('imagecache', 'ImageCache already generating: %dst, Lock file: %tmp.', array('%dst' => $dst, '%tmp' => $lockfile), WATCHDOG_NOTICE);
cf662f2d 399 // 307 Temporary Redirect, to myself. Lets hope the image is done next time around.
4f03d1f4 400 header('Location: '. request_uri(), TRUE, 307);
42669fda
DP
401 exit;
402 }
44eb6ee1 403 touch($lockfile);
23c2a416 404 // register the shtdown function to clean up lock files. by the time shutdown
405 // functions are being called the cwd has changed from document root, to
cf662f2d 406 // server root so absolute paths must be used for files in shutdown functions.
7d06010f 407 register_shutdown_function('file_delete', realpath($lockfile));
44eb6ee1 408
42669fda 409 // check if deriv exists... (file was created between apaches request handler and reaching this code)
8c361d7c 410 // otherwise try to create the derivative.
cf662f2d
DP
411 if (file_exists($dst) || imagecache_build_derivative($preset['actions'], $src, $dst)) {
412 imagecache_transfer($dst);
8c361d7c 413 }
cf662f2d
DP
414 // Generate an error if image could not generate.
415 watchdog('imagecache', 'Failed generating an image from %image using imagecache preset %preset.', array('%image' => $path, '%preset' => $preset['presetname']), WATCHDOG_ERROR);
416 header("HTTP/1.0 500 Internal Server Error");
417 exit;
42669fda 418}
1c784f3b 419
2bd4fda4 420/**
421 * Apply an action to an image.
422 *
423 * @param $action
424 * Action array
425 * @param $image
426 * Image object
427 * @return
428 * Boolean, true indicating success and false failure.
429 */
430function _imagecache_apply_action($action, $image) {
650d9de9 431 $actions = imagecache_action_definitions();
8c361d7c 432
1ae0e73d 433 if ($definition = imagecache_action_definition($action['action'])) {
2bd4fda4 434 $function = $action['action'] .'_image';
435 if (function_exists($function)) {
436 return $function($image, $action['data']);
437 }
650d9de9
DP
438 }
439 // skip undefined actions.. module probably got uninstalled or disabled.
8c361d7c 440 watchdog('imagecache', 'non-existant action %action', array('%action' => $action['action']), WATCHDOG_NOTICE);
67ee60c4 441 return true;
650d9de9
DP
442}
443
42669fda
DP
444/**
445 * helper function to transfer files from imagecache. Determines mime type and sets a last modified header.
446 * @param $path path to file to be transferred.
447 * @return <exit>
448 */
1c784f3b 449
42669fda 450function imagecache_transfer($path) {
25bb6d26 451 $size = getimagesize($path);
452 $headers = array('Content-Type: '. mime_header_encode($size['mime']));
12c7ea17 453
42669fda 454 if ($fileinfo = stat($path)) {
12c7ea17
DP
455 $headers[] = 'Content-Length: '. $fileinfo[7];
456 _imagecache_cache_set_cache_headers($fileinfo, $headers);
3edf6c97 457 }
42669fda
DP
458 file_transfer($path, $headers);
459 exit;
460}
3edf6c97 461
650d9de9 462/**
12c7ea17
DP
463 * Set file headers that handle "If-Modified-Since" correctly for the
464 * given fileinfo. Most code has been taken from drupal_page_cache_header().
465 */
466function _imagecache_cache_set_cache_headers($fileinfo, &$headers) {
467 // Set default values:
468 $last_modified = gmdate('D, d M Y H:i:s', $fileinfo[9]) .' GMT';
469 $etag = '"'. md5($last_modified) .'"';
470
471 // See if the client has provided the required HTTP headers:
472 $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
67ee60c4
DP
473 ? stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE'])
474 : false;
12c7ea17 475 $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH'])
67ee60c4
DP
476 ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH'])
477 : false;
12c7ea17
DP
478
479 if ($if_modified_since && $if_none_match
480 && $if_none_match == $etag // etag must match
481 && $if_modified_since == $last_modified) { // if-modified-since must match
482 header('HTTP/1.1 304 Not Modified');
483 // All 304 responses must send an etag if the 200 response
484 // for the same object contained an etag
485 header('Etag: '. $etag);
486 // We must also set Last-Modified again, so that we overwrite Drupal's
487 // default Last-Modified header with the right one
488 header('Last-Modified: '. $last_modified);
489 exit;
490 }
491
492 // Send appropriate response:
493 $headers[] = 'Last-Modified: '. $last_modified;
494 $headers[] = 'ETag: '. $etag;
495}
496
497/**
42669fda 498 * build an image cache derivative
23c2a416 499 *
12c7ea17
DP
500 * @param $actions Array of imagecache actions.
501 * @param $src Path of the source file.
502 * @param $dst Path of the destination file.
503 * @param $tmp Path of the temporary file used for manipulating the image.
67ee60c4 504 * @return true - derivative generated, false - no derivative generated, null - derivative being generated
42669fda 505 */
44eb6ee1 506function imagecache_build_derivative($actions, $src, $dst) {
42669fda
DP
507 // get the folder for the final location of this preset...
508 $dir = dirname($dst);
fa508ec3 509
42669fda 510 // Build the destination folder tree if it doesn't already exists.
23c2a416 511 if (!file_check_directory($dir, FILE_CREATE_DIRECTORY) && !mkdir($dir, 0775, true)) {
8c361d7c 512 watchdog('imagecache', 'Failed to create imagecache directory: %dir', array('%dir' => $dir), WATCHDOG_ERROR);
67ee60c4 513 return false;
3edf6c97 514 }
277b7f55 515
d4be5c6a 516 // Simply copy the file if there are no actions.
517 if (empty($actions)) {
518 return file_copy($src, $dst, FILE_EXISTS_REPLACE);
519 }
520
44eb6ee1 521 if (!$image = imageapi_image_open($src)) {
23c2a416 522 return false;
44eb6ee1
DP
523 }
524
42669fda 525 foreach ($actions as $action) {
9ba6a64d
DP
526 if (!empty($action['data'])) {
527 // QuickSketch, why do these run first/twice? - dopry.
528 if (isset($action['data']['width'])) {
529 $width = _imagecache_filter('width', $action['data']['width'], $image->info['width'], $image->info['height']);
530 }
531 if (isset($action['data']['height'])) {
532 $height = _imagecache_filter('height', $action['data']['height'], $image->info['width'], $image->info['height']);
533 }
f3ede8cc 534 foreach ($action['data'] as $key => $value) {
9ba6a64d 535 $action['data'][$key] = _imagecache_filter($key, $value, $image->info['width'], $image->info['height'], $width, $height);
f3ede8cc 536 }
1c784f3b 537 }
f490ff7d 538 if (!_imagecache_apply_action($action, $image)) {
8c361d7c 539 watchdog('imagecache', 'action(id:%id): %action failed for %src', array('%id' => $action['actionid'], '%action' => $action['action'], '%src' => $src), WATCHDOG_ERROR);
67ee60c4 540 return false;
1c784f3b 541 }
1c784f3b 542 }
9ba6a64d 543
44eb6ee1 544 if (!imageapi_image_close($image, $dst)) {
9ba6a64d 545 if (file_exists($dst)) {
618718c4 546 watchdog('imagecache', 'Cached image file %dst already exists. There may be an issue with your rewrite configuration.', array('%dst' => $dst), WATCHDOG_ERROR);
9ba6a64d 547 }
67ee60c4 548 return false;
650d9de9 549 }
8e83c5be 550
67ee60c4 551 return true;
1c784f3b
DP
552}
553
7a373970 554/**
555 * Implementation of hook_user().
556 */
557function imagecache_user($op, &$edit, &$account, $category = NULL) {
558 // Flush cached old user picture.
559 if ($op == 'update' && !empty($account->picture)) {
560 imagecache_image_flush($account->picture);
561 }
562}
2bd4fda4 563
7a373970 564/**
565 * Implementation of hook_imagefield_file().
566 */
5c9ebf68 567function imagecache_imagefield_file($op, $file) {
65312e36 568 switch ($op) {
5c9ebf68
DP
569 // Delete imagecache presets when imagecache images are deleted.
570 case 'delete': imagecache_image_flush($file['filepath']); break;
42669fda 571
8c361d7c
DP
572 // Create imagecache derivatives when files are saved.
573 case 'save':
574 break;
5c9ebf68
DP
575 }
576
577}
42669fda 578
1c784f3b
DP
579/**
580 * Implementation of hook_field_formatter_info().
77ba1b40
DP
581 *
582 * imagecache formatters are named as $presetname_$style
583 * $style is used to determine how the preset should be rendered.
584 * If you are implementing custom imagecache formatters please treat _ as
585 * reserved.
586 *
587 * @todo: move the linking functionality up to imagefield and clean up the default image
588 * integration.
1c784f3b
DP
589 */
590function imagecache_field_formatter_info() {
8c361d7c 591 $formatters = array();
42cc9535 592 foreach (imagecache_presets() as $preset) {
2acf221d 593 $formatters[$preset['presetname'] .'_default'] = array(
9cbdfdee 594 'label' => t('@preset image', array('@preset' => $preset['presetname'])),
ffb91456 595 'field types' => array('image', 'filefield'),
1c784f3b 596 );
42cc9535 597 $formatters[$preset['presetname'] .'_linked'] = array(
9cbdfdee 598 'label' => t('@preset image linked to node', array('@preset' => $preset['presetname'])),
ffb91456 599 'field types' => array('image', 'filefield'),
71ea9e27
DP
600 );
601 $formatters[$preset['presetname'] .'_imagelink'] = array(
9cbdfdee 602 'label' => t('@preset image linked to image', array('@preset' => $preset['presetname'])),
ffb91456 603 'field types' => array('image', 'filefield'),
1c784f3b 604 );
19fefb49 605 $formatters[$preset['presetname'] .'_path'] = array(
9cbdfdee 606 'label' => t('@preset file path', array('@preset' => $preset['presetname'])),
ffb91456 607 'field types' => array('image', 'filefield'),
608 );
77ba1b40 609 $formatters[$preset['presetname'] .'_url'] = array(
9cbdfdee 610 'label' => t('@preset URL', array('@preset' => $preset['presetname'])),
ffb91456 611 'field types' => array('image', 'filefield'),
612 );
1c784f3b
DP
613 }
614 return $formatters;
615}
616
8c361d7c 617function theme_imagecache_formatter($element) {
79c1f71d 618 if (isset($element['#item']['nid']) && $node = node_load($element['#item']['nid'])) {
619 return imagecache_field_formatter($element['#field_name'], $element['#item'], $element['#formatter'], $node);
620 }
8c361d7c
DP
621}
622
623
1c784f3b
DP
624/**
625 * Implementation of hook_field_formatter().
626 */
41b32b37 627function imagecache_field_formatter($field, $item, $formatter, $node) {
f490ff7d 628 if (empty($item['fid']) && $field['use_default_image']) {
8c361d7c 629 $item = $field['default_image'];
1c784f3b
DP
630 }
631 // Views does not load the file for us, while CCK display fields does.
79c1f71d 632 if (empty($item['filepath'])) {
c5ea02b0 633 $item = array_merge($item, field_file_load($item['fid']));
1c784f3b 634 }
f46a449a 635 if (is_string($item['data'])) {
636 $item['data'] = unserialize($item['data']);
637 }
34eb1998
DP
638 $alt = empty($item['data']['alt']) ? '' : $item['data']['alt'];
639 $title = empty($item['data']['title']) ? '' : $item['data']['title'];
f4266505
DP
640 $parts = explode('_', $formatter);
641 $style = array_pop($parts);
642 $presetname = implode('_', $parts);
23c2a416 643
77ba1b40 644 $class = "imagecache imagecache-$presetname imagecache-$style imagecache-$formatter";
42cc9535 645 if ($preset = imagecache_preset_by_name($presetname)) {
77ba1b40 646 $item['filepath'] = $item['fid'] == 'upload' ? $item['preview'] : $item['filepath'];
65312e36 647 switch ($style) {
77ba1b40 648 case 'linked':
34eb1998 649 $imagetag = theme('imagecache', $presetname, $item['filepath'], $alt, $title);
8c361d7c 650 return l($imagetag, 'node/'. $node->nid, array('attributes' => array('class' => $class), 'html' => true));
23c2a416 651
77ba1b40
DP
652 case 'imagelink':
653 $original_image_url = file_create_url($item['filepath']);
34eb1998 654 $imagetag = theme('imagecache', $presetname, $item['filepath'], $alt, $title);
8c361d7c 655 return l($imagetag, $original_image_url, array('attributes' => array('class' => $class), 'html' => true));
77ba1b40
DP
656
657 case 'url':
658 return imagecache_create_url($presetname, $item['filepath']);
659
19fefb49
DP
660 case 'path':
661 return imagecache_create_path($presetname, $item['filepath']);
662
77ba1b40 663 default:
76189102 664 return theme('imagecache', $presetname, $item['filepath'], $alt, $title, array('class' => $class));
77ba1b40 665 }
1c784f3b 666 }
19fefb49 667 return '<!-- imagecache formatter preset('. $presetname .') not found! -->';
1c784f3b
DP
668}
669
1c784f3b
DP
670/**
671 * Filter key word values such as 'top', 'right', 'center', and also percentages.
672 * All returned values are in pixels relative to the passed in height and width.
673 */
67ee60c4 674function _imagecache_filter($key, $value, $current_width, $current_height, $new_width = null, $new_height = null) {
1c784f3b
DP
675 switch ($key) {
676 case 'width':
677 $value = _imagecache_percent_filter($value, $current_width);
678 break;
679 case 'height':
680 $value = _imagecache_percent_filter($value, $current_height);
681 break;
682 case 'xoffset':
683 $value = _imagecache_keyword_filter($value, $current_width, $new_width);
684 break;
685 case 'yoffset':
686 $value = _imagecache_keyword_filter($value, $current_height, $new_height);
687 break;
688 }
689 return $value;
690}
691
692/**
693 * Accept a percentage and return it in pixels.
694 */
695function _imagecache_percent_filter($value, $current_pixels) {
67ee60c4 696 if (strpos($value, '%') !== false) {
1c784f3b
DP
697 $value = str_replace('%', '', $value) * 0.01 * $current_pixels;
698 }
699 return $value;
700}
701
702/**
703 * Accept a keyword (center, top, left, etc) and return it as an offset in pixels.
704 */
705function _imagecache_keyword_filter($value, $current_pixels, $new_pixels) {
706 switch ($value) {
707 case 'top':
708 case 'left':
709 $value = 0;
710 break;
711 case 'bottom':
712 case 'right':
713 $value = $current_pixels - $new_pixels;
714 break;
715 case 'center':
716 $value = $current_pixels/2 - $new_pixels/2;
717 break;
718 }
719 return $value;
720}
721
2d6b7863 722/**
650d9de9 723 * Recursively delete all files and folders in the specified filepath, then
1c784f3b
DP
724 * delete the containing folder.
725 *
726 * Note that this only deletes visible files with write permission.
2d6b7863
DP
727 *
728 * @param string $path
1c784f3b 729 * A filepath relative to file_directory_path.
2d6b7863 730 */
30e79786
DP
731function _imagecache_recursive_delete($path) {
732 if (is_file($path) || is_link($path)) {
733 unlink($path);
23c2a416 734 }
30e79786
DP
735 elseif (is_dir($path)) {
736 $d = dir($path);
737 while (($entry = $d->read()) !== false) {
738 if ($entry == '.' || $entry == '..') continue;
739 $entry_path = $path .'/'. $entry;
740 _imagecache_recursive_delete($entry_path);
741 }
66c2942d 742 $d->close();
30e79786
DP
743 rmdir($path);
744 }
745 else {
23c2a416 746 watchdog('imagecache', 'Unknown file type(%path) stat: %stat ',
30e79786
DP
747 array('%path' => $path, '%stat' => print_r(stat($path),1)), WATCHDOG_ERROR);
748 }
23c2a416 749
e0cb144b
DP
750}
751
e0cb144b 752/**
77ba1b40
DP
753 * Create and image tag for an imagecache derivative
754 *
755 * @param $namespace
756 * presetname of the derivative you wish to generate a tag for.
757 * @param $path
758 * path to the original image you wish to create a derivative image tag for.
759 * @param $alt
760 * img tag alternate text
761 * @param $title
762 * img tag title text
763 * @param attributes
23c2a416 764 * optional drupal attributes array. If attributes is set, the default imagecache classes
77ba1b40 765 * will not be set automatically, you must do this manually.
30fa0037 766 */
8c361d7c
DP
767function theme_imagecache($namespace, $path, $alt = '', $title = '', $attributes = NULL) {
768 if ($image = image_get_info(imagecache_create_path($namespace, $path))) {
769 $attributes['width'] = $image['width'];
770 $attributes['height'] = $image['height'];
771 }
77ba1b40 772 // check is_null so people can intentionally pass an empty array of attributes to override
23c2a416 773 // the defaults completely... if
77ba1b40
DP
774 if (is_null($attributes)) {
775 $attributes['class'] = 'imagecache imagecache-'. $namespace;
23c2a416 776 }
b59dc14e 777 $attributes = drupal_attributes($attributes);
77ba1b40
DP
778 $imagecache_url = imagecache_create_url($namespace, $path);
779 return '<img src="'. $imagecache_url .'" alt="'. check_plain($alt) .'" title="'. check_plain($title) .'" '. $attributes .' />';
505fd29e 780}
b9f4e1af 781
8c361d7c
DP
782function theme_imagecache_imagelink($namespace, $path, $alt = '', $title = '', $attributes = NULL) {
783 $image = theme('imagecache', $namespace, $path, $alt, $title);
784 $original_image_url = file_create_url($path);
785 return l($image, $original_image_url, array('absolute' => FALSE, 'html' => TRUE));
786}
71ea9e27 787
650d9de9 788
42cc9535
DP
789/************************************************************************************
790 * ImageCache action implementation example in module.
791 */
f490ff7d
DP
792function imagecache_resize_image(&$image, $data) {
793 if (!imageapi_image_resize($image, $data['width'], $data['height'])) {
8c361d7c 794 watchdog('imagecache', 'imagecache_resize_image failed. image: %image, data: %data.', array('%path' => $image, '%data' => print_r($data, TRUE)), WATCHDOG_ERROR);
67ee60c4 795 return false;
650d9de9 796 }
67ee60c4 797 return true;
650d9de9
DP
798}
799
800function imagecache_resize_form($action) {
801 $form['width'] = array(
802 '#type' => 'textfield',
803 '#title' => t('Width'),
8c361d7c 804 '#default_value' => isset($action['width']) ? $action['width'] : '100%',
650d9de9
DP
805 '#description' => t('Enter a width in pixels or as a percentage. i.e. 500 or 80%.'),
806 );
807 $form['height'] = array(
808 '#type' => 'textfield',
809 '#title' => t('Height'),
8c361d7c 810 '#default_value' => isset($action['height']) ? $action['height'] : '100%',
650d9de9
DP
811 '#description' => t('Enter a height in pixels or as a percentage. i.e. 500 or 80%.'),
812 );
813 return $form;
814}
815
42cc9535
DP
816function theme_imagecache_resize($element) {
817 $data = $element['#value'];
818 return 'width: '. $data['width'] .', height: '. $data['height'];
819}
820
821
822
823/**
824 * ImageCache 2.x API
825 *
23c2a416 826 * The API for imagecache has changed. The 2.x API returns more structured
827 * data, has shorter function names, and implements more aggressive metadata
88a6cefc 828 * caching.
42cc9535
DP
829 *
830 */
831
832/**
833 * Get an array of all presets and their settings.
834 *
835 * @param reset
836 * if set to true it will clear the preset cache
8c361d7c 837 *
42cc9535
DP
838 * @return
839 * array of presets array( $preset_id => array('presetid' => integer, 'presetname' => string))
840 */
67ee60c4 841function imagecache_presets($reset = false) {
42cc9535
DP
842 static $presets = array();
843
67ee60c4 844 // Clear caches if $reset is true;
42cc9535
DP
845 if ($reset) {
846 $presets = array();
847 cache_clear_all('imagecache:presets', 'cache');
848
849 // Clear the content.module cache (refreshes the list of formatters provided by imagefield.module).
850 if (module_exists('content')) {
851 content_clear_type_cache();
852 }
853 }
42cc9535
DP
854 // Return presets if the array is populated.
855 if (!empty($presets)) {
856 return $presets;
857 }
858
dcc340ad 859 // Grab from cache or build the array. To ensure that the Drupal 5 upgrade
860 // path works, we also check whether the presets list is an array.
861 if (($cache = cache_get('imagecache:presets', 'cache')) && is_array($cache->data)) {
8c361d7c 862 $presets = $cache->data;
42cc9535
DP
863 }
864 else {
6624dc14 865 $normal_presets = array();
866
42cc9535
DP
867 $result = db_query('SELECT * FROM {imagecache_preset} ORDER BY presetname');
868 while ($preset = db_fetch_array($result)) {
869 $presets[$preset['presetid']] = $preset;
870 $presets[$preset['presetid']]['actions'] = imagecache_preset_actions($preset);
6624dc14 871 $presets[$preset['presetid']]['storage'] = IMAGECACHE_STORAGE_NORMAL;
872
873 // Collect normal preset names so we can skip defaults and mark overrides accordingly
874 $normal_presets[$preset['presetname']] = $preset['presetid'];
42cc9535 875 }
6624dc14 876
877 // Collect default presets and allow modules to modify them before they
878 // are cached.
879 $default_presets = module_invoke_all('imagecache_default_presets');
880 drupal_alter('imagecache_default_presets', $default_presets);
881
882 // Add in default presets if they don't conflict with any normal presets.
883 // Mark normal presets that take the same preset namespace as overrides.
884 foreach ($default_presets as $preset) {
885 if (!empty($preset['presetname'])) {
886 if (!isset($normal_presets[$preset['presetname']])) {
887 $preset['storage'] = IMAGECACHE_STORAGE_DEFAULT;
888 // Use a string preset identifier
889 $preset['presetid'] = $preset['presetname'];
890 $presets[$preset['presetname']] = $preset;
891 }
892 else {
893 $presetid = $normal_presets[$preset['presetname']];
894 $presets[$presetid]['storage'] = IMAGECACHE_STORAGE_OVERRIDE;
895 }
896 }
897 }
898
8c361d7c 899 cache_set('imagecache:presets', $presets);
42cc9535
DP
900 }
901 return $presets;
902}
903
904/**
905 * Load a preset by preset_id.
906 *
907 * @param preset_id
908 * The numeric id of a preset.
8c361d7c 909 *
42cc9535
DP
910 * @return
911 * preset array( 'presetname' => string, 'presetid' => integet)
912 * empty array if preset_id is an invalid preset
913 */
67ee60c4 914function imagecache_preset($preset_id, $reset = false) {
42cc9535
DP
915 $presets = imagecache_presets($reset);
916 return (isset($presets[$preset_id])) ? $presets[$preset_id] : array();
917}
918
919/**
920 * Load a preset by name.
921 *
922 * @param preset_name
923 *
924 * @return
925 * preset array( 'presetname' => string, 'presetid' => integer)
926 * empty array if preset_name is an invalid preset
927 */
8c361d7c 928
42cc9535
DP
929function imagecache_preset_by_name($preset_name) {
930 static $presets_by_name = array();
931 if (!$presets_by_name && $presets = imagecache_presets()) {
932 foreach ($presets as $preset) {
933 $presets_by_name[$preset['presetname']] = $preset;
934 }
935 }
8c361d7c 936 return (isset($presets_by_name[$preset_name])) ? $presets_by_name[$preset_name] : array();
42cc9535
DP
937}
938
939/**
940 * Save an ImageCache preset.
941 *
942 * @param preset
943 * an imagecache preset array.
944 * @return
945 * a preset array. In the case of a new preset, 'presetid' will be populated.
946 */
947function imagecache_preset_save($preset) {
948 // @todo: CRUD level validation?
949 if (isset($preset['presetid']) && is_numeric($preset['presetid'])) {
8c361d7c 950 drupal_write_record('imagecache_preset', $preset, 'presetid');
42cc9535
DP
951 }
952 else {
8c361d7c 953 drupal_write_record('imagecache_preset', $preset);
42cc9535
DP
954 }
955
956 // Reset presets cache.
957 imagecache_preset_flush($preset);
67ee60c4 958 imagecache_presets(true);
8c361d7c
DP
959
960 // Rebuild Theme Registry
961 drupal_rebuild_theme_registry();
962
42cc9535
DP
963 return $preset;
964}
965
966function imagecache_preset_delete($preset) {
967 imagecache_preset_flush($preset['presetid']);
968 db_query('DELETE FROM {imagecache_action} where presetid = %d', $preset['presetid']);
969 db_query('DELETE FROM {imagecache_preset} where presetid = %d', $preset['presetid']);
67ee60c4
DP
970 imagecache_presets(true);
971 return true;
42cc9535
DP
972}
973
67ee60c4 974function imagecache_preset_actions($preset, $reset = false) {
42cc9535
DP
975 static $actions_cache = array();
976
977 if ($reset || empty($actions_cache[$preset['presetid']])) {
978 $result = db_query('SELECT * FROM {imagecache_action} where presetid = %d order by weight', $preset['presetid']);
979 while ($row = db_fetch_array($result)) {
980 $row['data'] = unserialize($row['data']);
981 $actions_cache[$preset['presetid']][] = $row;
8c361d7c 982 }
42cc9535
DP
983 }
984
985 return isset($actions_cache[$preset['presetid']]) ? $actions_cache[$preset['presetid']] : array();
986}
987
988/**
989 * Flush cached media for a preset.
990 * @param id
991 * A preset id.
992 */
993function imagecache_preset_flush($preset) {
994 if (user_access('flush imagecache')) {
995 $presetdir = realpath(file_directory_path() .'/imagecache/'. $preset['presetname']);
996 if (is_dir($presetdir)) {
997 _imagecache_recursive_delete($presetdir);
998 }
999 }
1000}
1001
1002/**
1003 * Clear cached versions of a specific file in all presets.
1004 * @param $path
1005 * The Drupal file path to the original image.
1006 */
1007function imagecache_image_flush($path) {
1008 foreach (imagecache_presets() as $preset) {
5c9ebf68 1009 file_delete(imagecache_create_path($preset['presetname'], $path));
42cc9535
DP
1010 }
1011}
1012
1013function imagecache_action($actionid) {
1ae0e73d
DP
1014 static $actions;
1015
1016 if (!isset($actions[$actionid])) {
1017 $action = array();
1018
1019 $result = db_query('SELECT * FROM {imagecache_action} WHERE actionid=%d', $actionid);
1020 if ($row = db_fetch_array($result)) {
1021 $action = $row;
1022 $action['data'] = unserialize($action['data']);
1023
1024 $definition = imagecache_action_definition($action['action']);
1025 $action = array_merge($definition, $action);
1026 $actions[$actionid] = $action;
1027 }
8c361d7c 1028 }
1ae0e73d 1029 return $actions[$actionid];
42cc9535
DP
1030}
1031
8c361d7c
DP
1032function imagecache_action_load($actionid) {
1033 return imagecache_action($actionid, TRUE);
1034}
1035
42cc9535 1036function imagecache_action_save($action) {
34eb1998
DP
1037 $definition = imagecache_action_definition($action['action']);
1038 $action = array_merge($definition, $action);
1039
8c361d7c
DP
1040 if (!empty($action['actionid'])) {
1041 drupal_write_record('imagecache_action', $action, 'actionid');
42cc9535
DP
1042 }
1043 else {
8c361d7c 1044 drupal_write_record('imagecache_action', $action);
42cc9535
DP
1045 }
1046 $preset = imagecache_preset($action['presetid']);
1047 imagecache_preset_flush($preset);
67ee60c4 1048 imagecache_presets(true);
23c2a416 1049 return $action;
42cc9535
DP
1050}
1051
1052function imagecache_action_delete($action) {
67ee60c4 1053 db_query('DELETE FROM {imagecache_action} WHERE actionid=%d', $action['actionid']);
42cc9535
DP
1054 $preset = imagecache_preset($action['presetid']);
1055 imagecache_preset_flush($preset);
67ee60c4 1056 imagecache_presets(true);
42cc9535 1057}