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