| 1 | <?php |
| 2 | // $Id: image.inc,v 1.33 2009/04/20 20:02:30 dries Exp $ |
| 3 | |
| 4 | /** |
| 5 | * @file |
| 6 | * API for manipulating images. |
| 7 | */ |
| 8 | |
| 9 | /** |
| 10 | * @defgroup image Image toolkits |
| 11 | * @{ |
| 12 | * Drupal's image toolkits provide an abstraction layer for common image file |
| 13 | * manipulations like scaling, cropping, and rotating. The abstraction frees |
| 14 | * module authors from the need to support multiple image libraries, and it |
| 15 | * allows site administrators to choose the library that's best for them. |
| 16 | * |
| 17 | * PHP includes the GD library by default so a GD toolkit is installed with |
| 18 | * Drupal. Other toolkits like ImageMagic are available from contrib modules. |
| 19 | * GD works well for small images, but using it with larger files may cause PHP |
| 20 | * to run out of memory. In contrast the ImageMagick library does not suffer |
| 21 | * from this problem, but it requires the ISP to have installed additional |
| 22 | * software. |
| 23 | * |
| 24 | * Image toolkits are discovered based on the associated module's |
| 25 | * hook_image_toolkits. Additionally the image toolkit include file |
| 26 | * must be identified in the files array in the module.info file. The |
| 27 | * toolkit must then be enabled using the admin/settings/image-toolkit |
| 28 | * form. |
| 29 | * |
| 30 | * Only one toolkit may be selected at a time. If a module author wishes to call |
| 31 | * a specific toolkit they can check that it is installed by calling |
| 32 | * image_get_available_toolkits(), and then calling its functions directly. |
| 33 | */ |
| 34 | |
| 35 | /** |
| 36 | * Return a list of available toolkits. |
| 37 | * |
| 38 | * @return |
| 39 | * An array with the toolkit names as keys and the descriptions as values. |
| 40 | */ |
| 41 | function image_get_available_toolkits() { |
| 42 | // hook_image_toolkits returns an array of toolkit names. |
| 43 | $toolkits = module_invoke_all('image_toolkits'); |
| 44 | |
| 45 | $output = array(); |
| 46 | foreach ($toolkits as $name => $info) { |
| 47 | // Only allow modules that aren't marked as unavailable. |
| 48 | if ($info['available']) { |
| 49 | $output[$name] = $info['title']; |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | return $output; |
| 54 | } |
| 55 | |
| 56 | /** |
| 57 | * Retrieve the name of the currently used toolkit. |
| 58 | * |
| 59 | * @return |
| 60 | * String containing the name of the selected toolkit, or FALSE on error. |
| 61 | */ |
| 62 | function image_get_toolkit() { |
| 63 | static $toolkit; |
| 64 | |
| 65 | if (!isset($toolkit)) { |
| 66 | $toolkits = image_get_available_toolkits(); |
| 67 | $toolkit = variable_get('image_toolkit', 'gd'); |
| 68 | if (!isset($toolkits[$toolkit]) || !drupal_function_exists('image_' . $toolkit . '_load')) { |
| 69 | // The selected toolkit isn't available so return the first one found. If |
| 70 | // none are available this will return FALSE. |
| 71 | reset($toolkits); |
| 72 | $toolkit = key($toolkits); |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | return $toolkit; |
| 77 | } |
| 78 | |
| 79 | /** |
| 80 | * Invokes the given method using the currently selected toolkit. |
| 81 | * |
| 82 | * @param $method |
| 83 | * A string containing the method to invoke. |
| 84 | * @param $image |
| 85 | * An image object returned by image_load(). |
| 86 | * @param $params |
| 87 | * An optional array of parameters to pass to the toolkit method. |
| 88 | * @return |
| 89 | * Mixed values (typically Boolean indicating successful operation). |
| 90 | */ |
| 91 | function image_toolkit_invoke($method, stdClass $image, array $params = array()) { |
| 92 | $function = 'image_' . $image->toolkit . '_' . $method; |
| 93 | if (drupal_function_exists($function)) { |
| 94 | array_unshift($params, $image); |
| 95 | return call_user_func_array($function, $params); |
| 96 | } |
| 97 | watchdog('image', 'The selected image handling toolkit %toolkit can not correctly process %function.', array('%toolkit' => $image->toolkit, '%function' => $function), WATCHDOG_ERROR); |
| 98 | return FALSE; |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * Get details about an image. |
| 103 | * |
| 104 | * Drupal only supports GIF, JPG and PNG file formats. |
| 105 | * |
| 106 | * @param $filepath |
| 107 | * String specifying the path of the image file. |
| 108 | * @return |
| 109 | * FALSE, if the file could not be found or is not an image. Otherwise, a |
| 110 | * keyed array containing information about the image: |
| 111 | * 'width' - Width, in pixels. |
| 112 | * 'height' - Height, in pixels. |
| 113 | * 'extension' - Commonly used file extension for the image. |
| 114 | * 'mime_type' - MIME type ('image/jpeg', 'image/gif', 'image/png'). |
| 115 | * 'file_size' - File size in bytes. |
| 116 | */ |
| 117 | function image_get_info($filepath) { |
| 118 | if (!is_file($filepath)) { |
| 119 | return FALSE; |
| 120 | } |
| 121 | |
| 122 | $details = FALSE; |
| 123 | $data = @getimagesize($filepath); |
| 124 | $file_size = @filesize($filepath); |
| 125 | |
| 126 | if (isset($data) && is_array($data)) { |
| 127 | $extensions = array('1' => 'gif', '2' => 'jpg', '3' => 'png'); |
| 128 | $extension = array_key_exists($data[2], $extensions) ? $extensions[$data[2]] : ''; |
| 129 | $details = array('width' => $data[0], |
| 130 | 'height' => $data[1], |
| 131 | 'extension' => $extension, |
| 132 | 'file_size' => $file_size, |
| 133 | 'mime_type' => $data['mime']); |
| 134 | } |
| 135 | |
| 136 | return $details; |
| 137 | } |
| 138 | |
| 139 | /** |
| 140 | * Scales an image to the exact width and height given. |
| 141 | * |
| 142 | * This function achieves the target aspect ratio by cropping the original image |
| 143 | * equally on both sides, or equally on the top and bottom. This function is |
| 144 | * useful to create uniform sized avatars from larger images. |
| 145 | * |
| 146 | * The resulting image always has the exact target dimensions. |
| 147 | * |
| 148 | * @param $image |
| 149 | * An image object returned by image_load(). |
| 150 | * @param $width |
| 151 | * The target width, in pixels. |
| 152 | * @param $height |
| 153 | * The target height, in pixels. |
| 154 | * @return |
| 155 | * TRUE or FALSE, based on success. |
| 156 | * |
| 157 | * @see image_load() |
| 158 | * @see image_resize() |
| 159 | * @see image_crop() |
| 160 | */ |
| 161 | function image_scale_and_crop(stdClass $image, $width, $height) { |
| 162 | $scale = max($width / $image->info['width'], $height / $image->info['height']); |
| 163 | $x = ($image->info['width'] * $scale - $width) / 2; |
| 164 | $y = ($image->info['height'] * $scale - $height) / 2; |
| 165 | |
| 166 | if (image_resize($image, $image->info['width'] * $scale, $image->info['height'] * $scale)) { |
| 167 | return image_crop($image, $x, $y, $width, $height); |
| 168 | } |
| 169 | return FALSE; |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Scales an image to the given width and height while maintaining aspect ratio. |
| 174 | * |
| 175 | * The resulting image can be smaller for one or both target dimensions. |
| 176 | * |
| 177 | * @param $image |
| 178 | * An image object returned by image_load(). |
| 179 | * @param $width |
| 180 | * The target width, in pixels. This value is omitted then the scaling will |
| 181 | * based only on the height value. |
| 182 | * @param $height |
| 183 | * The target height, in pixels. This value is omitted then the scaling will |
| 184 | * based only on the width value. |
| 185 | * @param $upscale |
| 186 | * Boolean indicating that files smaller than the dimensions will be scalled |
| 187 | * up. This generally results in a low quality image. |
| 188 | * @return |
| 189 | * TRUE or FALSE, based on success. |
| 190 | * |
| 191 | * @see image_load() |
| 192 | * @see image_scale_and_crop() |
| 193 | */ |
| 194 | function image_scale(stdClass $image, $width = NULL, $height = NULL, $upscale = FALSE) { |
| 195 | $aspect = $image->info['height'] / $image->info['width']; |
| 196 | |
| 197 | if ($upscale) { |
| 198 | // Set width/height according to aspect ratio if either is empty. |
| 199 | $width = !empty($width) ? $width : $height / $aspect; |
| 200 | $height = !empty($height) ? $height : $width / $aspect; |
| 201 | } |
| 202 | else { |
| 203 | // Set impossibly large values if the width and height aren't set. |
| 204 | $width = !empty($width) ? $width : 9999999; |
| 205 | $height = !empty($height) ? $height : 9999999; |
| 206 | |
| 207 | // Don't scale up. |
| 208 | if (round($width) >= $image->info['width'] && round($height) >= $image->info['height']) { |
| 209 | return TRUE; |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | if ($aspect < $height / $width) { |
| 214 | $height = $width * $aspect; |
| 215 | } |
| 216 | else { |
| 217 | $width = $height / $aspect; |
| 218 | } |
| 219 | |
| 220 | return image_resize($image, $width, $height); |
| 221 | } |
| 222 | |
| 223 | /** |
| 224 | * Resize an image to the given dimensions (ignoring aspect ratio). |
| 225 | * |
| 226 | * @param $image |
| 227 | * An image object returned by image_load(). |
| 228 | * @param $width |
| 229 | * The target width, in pixels. |
| 230 | * @param $height |
| 231 | * The target height, in pixels. |
| 232 | * @return |
| 233 | * TRUE or FALSE, based on success. |
| 234 | * |
| 235 | * @see image_load() |
| 236 | * @see image_gd_resize() |
| 237 | */ |
| 238 | function image_resize(stdClass $image, $width, $height) { |
| 239 | $width = (int) round($width); |
| 240 | $height = (int) round($height); |
| 241 | |
| 242 | return image_toolkit_invoke('resize', $image, array($width, $height)); |
| 243 | } |
| 244 | |
| 245 | /** |
| 246 | * Rotate an image by the given number of degrees. |
| 247 | * |
| 248 | * @param $image |
| 249 | * An image object returned by image_load(). |
| 250 | * @param $degrees |
| 251 | * The number of (clockwise) degrees to rotate the image. |
| 252 | * @param $background |
| 253 | * An hexadecimal integer specifying the background color to use for the |
| 254 | * uncovered area of the image after the rotation. E.g. 0x000000 for black, |
| 255 | * 0xff00ff for magenta, and 0xffffff for white. For images that support |
| 256 | * transparency, this will default to transparent. Otherwise it will |
| 257 | * be white. |
| 258 | * @return |
| 259 | * TRUE or FALSE, based on success. |
| 260 | * |
| 261 | * @see image_load() |
| 262 | * @see image_gd_rotate() |
| 263 | */ |
| 264 | function image_rotate(stdClass $image, $degrees, $background = NULL) { |
| 265 | return image_toolkit_invoke('rotate', $image, array($degrees, $background)); |
| 266 | } |
| 267 | |
| 268 | /** |
| 269 | * Crop an image to the rectangle specified by the given rectangle. |
| 270 | * |
| 271 | * @param $image |
| 272 | * An image object returned by image_load(). |
| 273 | * @param $x |
| 274 | * The top left coordinate, in pixels, of the crop area (x axis value). |
| 275 | * @param $y |
| 276 | * The top left coordinate, in pixels, of the crop area (y axis value). |
| 277 | * @param $width |
| 278 | * The target width, in pixels. |
| 279 | * @param $height |
| 280 | * The target height, in pixels. |
| 281 | * @return |
| 282 | * TRUE or FALSE, based on success. |
| 283 | * |
| 284 | * @see image_load() |
| 285 | * @see image_scale_and_crop() |
| 286 | * @see image_gd_crop() |
| 287 | */ |
| 288 | function image_crop(stdClass $image, $x, $y, $width, $height) { |
| 289 | $aspect = $image->info['height'] / $image->info['width']; |
| 290 | if (empty($height)) $height = $width / $aspect; |
| 291 | if (empty($width)) $width = $height * $aspect; |
| 292 | |
| 293 | $width = (int) round($width); |
| 294 | $height = (int) round($height); |
| 295 | |
| 296 | return image_toolkit_invoke('crop', $image, array($x, $y, $width, $height)); |
| 297 | } |
| 298 | |
| 299 | /** |
| 300 | * Convert an image to grayscale. |
| 301 | * |
| 302 | * @param $image |
| 303 | * An image object returned by image_load(). |
| 304 | * @return |
| 305 | * TRUE or FALSE, based on success. |
| 306 | * |
| 307 | * @see image_load() |
| 308 | * @see image_gd_desaturate() |
| 309 | */ |
| 310 | function image_desaturate(stdClass $image) { |
| 311 | return image_toolkit_invoke('desaturate', $image); |
| 312 | } |
| 313 | |
| 314 | |
| 315 | /** |
| 316 | * Load an image file and return an image object. |
| 317 | * |
| 318 | * Any changes to the file are not saved until image_save() is called. |
| 319 | * |
| 320 | * @param $file |
| 321 | * Path to an image file. |
| 322 | * @param $toolkit |
| 323 | * An optional, image toolkit name to override the default. |
| 324 | * @return |
| 325 | * An image object or FALSE if there was a problem loading the file. The |
| 326 | * image object has the following properties: |
| 327 | * - 'source' - The original file path. |
| 328 | * - 'info' - The array of information returned by image_get_info() |
| 329 | * - 'toolkit' - The name of the image toolkit requested when the image was |
| 330 | * loaded. |
| 331 | * Image tookits may add additional properties. The caller is advised not to |
| 332 | * monkey about with them. |
| 333 | * |
| 334 | * @see image_save() |
| 335 | * @see image_get_info() |
| 336 | * @see image_get_available_toolkits() |
| 337 | * @see image_gd_load() |
| 338 | */ |
| 339 | function image_load($file, $toolkit = FALSE) { |
| 340 | if (!$toolkit) { |
| 341 | $toolkit = image_get_toolkit(); |
| 342 | } |
| 343 | if ($toolkit) { |
| 344 | $image = new stdClass(); |
| 345 | $image->source = $file; |
| 346 | $image->info = image_get_info($file); |
| 347 | $image->toolkit = $toolkit; |
| 348 | if (image_toolkit_invoke('load', $image)) { |
| 349 | return $image; |
| 350 | } |
| 351 | } |
| 352 | return FALSE; |
| 353 | } |
| 354 | |
| 355 | /** |
| 356 | * Close the image and save the changes to a file. |
| 357 | * |
| 358 | * @param $image |
| 359 | * An image object returned by image_load(). The object's 'info' property |
| 360 | * will be updated if the file is saved successfully. |
| 361 | * @param $destination |
| 362 | * Destination path where the image should be saved. If it is empty the |
| 363 | * original image file will be overwritten. |
| 364 | * @return |
| 365 | * TRUE or FALSE, based on success. |
| 366 | * |
| 367 | * @see image_load() |
| 368 | * @see image_gd_save() |
| 369 | */ |
| 370 | function image_save(stdClass $image, $destination = NULL) { |
| 371 | if (empty($destination)) { |
| 372 | $destination = $image->source; |
| 373 | } |
| 374 | if ($return = image_toolkit_invoke('save', $image, array($destination))) { |
| 375 | // Clear the cached file size and refresh the image information. |
| 376 | clearstatcache(); |
| 377 | $image->info = image_get_info($destination); |
| 378 | |
| 379 | if (drupal_chmod($destination)) { |
| 380 | return $return; |
| 381 | } |
| 382 | } |
| 383 | return FALSE; |
| 384 | } |
| 385 | |
| 386 | /** |
| 387 | * @} End of "defgroup image". |
| 388 | */ |
| 389 |