diff --git a/core/includes/image.inc b/core/includes/image.inc index f0b91bf..34155fc 100644 --- a/core/includes/image.inc +++ b/core/includes/image.inc @@ -5,6 +5,8 @@ * API for manipulating images. */ +use Drupal\system\Plugin\ImageToolkitManager; + /** * @defgroup image Image toolkits * @{ @@ -39,15 +41,16 @@ * @return * An array with the toolkit names as keys and the descriptions as values. */ -function image_get_available_toolkits() { - // hook_image_toolkits returns an array of toolkit names. - $toolkits = module_invoke_all('image_toolkits'); +function image_get_available_toolkits($key = NULL) { + // Use plugin system to get list of available toolkits. + $manager = new ImageToolkitManager(); + $toolkits = $manager->getDefinitions(); $output = array(); - foreach ($toolkits as $name => $info) { + foreach ($toolkits as $id => $definition) { // Only allow modules that aren't marked as unavailable. - if ($info['available']) { - $output[$name] = $info['title']; + if (call_user_func($definition['class'] . '::checkAvailable')) { + $output[$id] = $definition; } } @@ -58,46 +61,32 @@ function image_get_available_toolkits() { * Gets the name of the currently used toolkit. * * @return - * String containing the name of the selected toolkit, or FALSE on error. + * Object of the default toolkit, or FALSE on error. */ -function image_get_toolkit() { - static $toolkit; +function image_get_toolkit($toolkit_id = NULL) { + static $toolkit = array(); + + if (empty($toolkit_id)) { + $toolkit_id = config('system.image')->get('toolkit'); + } - if (!isset($toolkit)) { + if (!isset($toolkit[$toolkit_id])) { $toolkits = image_get_available_toolkits(); - $toolkit = variable_get('image_toolkit', 'gd'); - if (!isset($toolkits[$toolkit]) || !function_exists('image_' . $toolkit . '_load')) { + + if (!isset($toolkits[$toolkit_id]) || !class_exists($toolkits[$toolkit_id]['class'])) { // The selected toolkit isn't available so return the first one found. If // none are available this will return FALSE. reset($toolkits); - $toolkit = key($toolkits); + $toolkit_id = key($toolkits); } - } - return $toolkit; -} - -/** - * Invokes the given method using the currently selected toolkit. - * - * @param $method - * A string containing the method to invoke. - * @param $image - * An image object returned by image_load(). - * @param $params - * An optional array of parameters to pass to the toolkit method. - * - * @return - * Mixed values (typically Boolean indicating successful operation). - */ -function image_toolkit_invoke($method, stdClass $image, array $params = array()) { - $function = 'image_' . $image->toolkit . '_' . $method; - if (function_exists($function)) { - array_unshift($params, $image); - return call_user_func_array($function, $params); + if ($toolkit_id) { + $manager = new ImageToolkitManager(); + $toolkit[$toolkit_id] = $manager->createInstance($toolkit_id); + } } - watchdog('image', 'The selected image handling toolkit %toolkit can not correctly process %function.', array('%toolkit' => $image->toolkit, '%function' => $function), WATCHDOG_ERROR); - return FALSE; + + return $toolkit[$toolkit_id]; } /** @@ -110,7 +99,7 @@ function image_toolkit_invoke($method, stdClass $image, array $params = array()) * @param $filepath * String specifying the path of the image file. * @param $toolkit - * An optional image toolkit name to override the default. + * An optional image toolkit object to override the default. * * @return * FALSE, if the file could not be found or is not an image. Otherwise, a @@ -134,7 +123,7 @@ function image_get_info($filepath, $toolkit = FALSE) { $image = new stdClass(); $image->source = $filepath; $image->toolkit = $toolkit; - $details = image_toolkit_invoke('get_info', $image); + $details = $toolkit->getInfo($image); if (isset($details) && is_array($details)) { $details['file_size'] = filesize($filepath); } @@ -280,7 +269,7 @@ function image_resize(stdClass $image, $width, $height) { $width = (int) round($width); $height = (int) round($height); - return image_toolkit_invoke('resize', $image, array($width, $height)); + return $image->toolkit->resize($image, $width, $height); } /** @@ -304,7 +293,7 @@ function image_resize(stdClass $image, $width, $height) { * @see image_gd_rotate() */ function image_rotate(stdClass $image, $degrees, $background = NULL) { - return image_toolkit_invoke('rotate', $image, array($degrees, $background)); + return $image->toolkit->rotate($image, $degrees, $background); } /** @@ -336,7 +325,7 @@ function image_crop(stdClass $image, $x, $y, $width, $height) { $width = (int) round($width); $height = (int) round($height); - return image_toolkit_invoke('crop', $image, array($x, $y, $width, $height)); + return $image->toolkit->crop($image, $x, $y, $width, $height); } /** @@ -352,7 +341,7 @@ function image_crop(stdClass $image, $x, $y, $width, $height) { * @see image_gd_desaturate() */ function image_desaturate(stdClass $image) { - return image_toolkit_invoke('desaturate', $image); + return $image->toolkit->desaturate($image); } /** @@ -363,7 +352,7 @@ function image_desaturate(stdClass $image) { * @param $file * Path to an image file. * @param $toolkit - * An optional, image toolkit name to override the default. + * An optional, image toolkit object to override the default. * * @return * An image object or FALSE if there was a problem loading the file. The @@ -390,7 +379,7 @@ function image_load($file, $toolkit = FALSE) { $image->info = image_get_info($file, $toolkit); if (isset($image->info) && is_array($image->info)) { $image->toolkit = $toolkit; - if (image_toolkit_invoke('load', $image)) { + if ($toolkit->load($image)) { return $image; } } @@ -418,7 +407,7 @@ function image_save(stdClass $image, $destination = NULL) { if (empty($destination)) { $destination = $image->source; } - if ($return = image_toolkit_invoke('save', $image, array($destination))) { + if ($return = $image->toolkit->save($image, $destination)) { // Clear the cached file size and refresh the image information. clearstatcache(TRUE, $destination); $image->info = image_get_info($destination, $image->toolkit); diff --git a/core/modules/image/image.effects.inc b/core/modules/image/image.effects.inc index 35a6a74..4f7379b 100644 --- a/core/modules/image/image.effects.inc +++ b/core/modules/image/image.effects.inc @@ -79,7 +79,7 @@ function image_image_effect_info() { */ function image_resize_effect(&$image, $data) { if (!image_resize($image, $data['width'], $data['height'])) { - watchdog('image', 'Image resize failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image resize failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => get_class($image->toolkit), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; @@ -130,7 +130,7 @@ function image_scale_effect(&$image, $data) { ); if (!image_scale($image, $data['width'], $data['height'], $data['upscale'])) { - watchdog('image', 'Image scale failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image scale failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => get_class($image->toolkit), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; @@ -184,7 +184,7 @@ function image_crop_effect(&$image, $data) { $x = image_filter_keyword($x, $image->info['width'], $data['width']); $y = image_filter_keyword($y, $image->info['height'], $data['height']); if (!image_crop($image, $x, $y, $data['width'], $data['height'])) { - watchdog('image', 'Image crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => get_class($image->toolkit), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; @@ -206,7 +206,7 @@ function image_crop_effect(&$image, $data) { */ function image_scale_and_crop_effect(&$image, $data) { if (!image_scale_and_crop($image, $data['width'], $data['height'])) { - watchdog('image', 'Image scale and crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image scale and crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => get_class($image->toolkit), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; @@ -225,7 +225,7 @@ function image_scale_and_crop_effect(&$image, $data) { */ function image_desaturate_effect(&$image, $data) { if (!image_desaturate($image)) { - watchdog('image', 'Image desaturate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image desaturate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => get_class($image->toolkit), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; @@ -278,7 +278,7 @@ function image_rotate_effect(&$image, $data) { } if (!image_rotate($image, $data['degrees'], $data['bgcolor'])) { - watchdog('image', 'Image rotate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image rotate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => get_class($image->toolkit), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php index fb3269c..b7a5b67 100644 --- a/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php +++ b/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php @@ -20,7 +20,7 @@ class ImageEffectsTest extends ToolkitTestBase { * * @var array */ - public static $modules = array('image_test'); + public static $modules = array('image_test', 'image'); public static function getInfo() { return array( diff --git a/core/modules/system/config/system.image.gd.yml b/core/modules/system/config/system.image.gd.yml new file mode 100644 index 0000000..fbc379f --- /dev/null +++ b/core/modules/system/config/system.image.gd.yml @@ -0,0 +1 @@ +jpeg_quality: '75' diff --git a/core/modules/system/config/system.image.yml b/core/modules/system/config/system.image.yml new file mode 100644 index 0000000..9a1688f --- /dev/null +++ b/core/modules/system/config/system.image.yml @@ -0,0 +1 @@ +toolkit: gd diff --git a/core/modules/system/image.gd.inc b/core/modules/system/image.gd.inc deleted file mode 100644 index f6f12ae..0000000 --- a/core/modules/system/image.gd.inc +++ /dev/null @@ -1,367 +0,0 @@ - t('The GD toolkit is installed and working properly.') - ); - - $form['image_jpeg_quality'] = array( - '#type' => 'number', - '#title' => t('JPEG quality'), - '#description' => t('Define the image quality for JPEG manipulations. Ranges from 0 to 100. Higher values mean better image quality but bigger files.'), - '#min' => 0, - '#max' => 100, - '#default_value' => variable_get('image_jpeg_quality', 75), - '#field_suffix' => t('%'), - ); - - return $form; - } - else { - form_set_error('image_toolkit', t('The GD image toolkit requires that the GD module for PHP be installed and configured properly. For more information see PHP\'s image documentation.', array('@url' => 'http://php.net/image'))); - return FALSE; - } -} - -/** - * Verify GD2 settings (that the right version is actually installed). - * - * @return - * A boolean indicating if the GD toolkit is available on this machine. - */ -function image_gd_check_settings() { - if ($check = get_extension_funcs('gd')) { - if (in_array('imagegd2', $check)) { - // GD2 support is available. - return TRUE; - } - } - return FALSE; -} - -/** - * Scale an image to the specified size using GD. - * - * @param $image - * An image object. The $image->resource, $image->info['width'], and - * $image->info['height'] values will be modified by this call. - * @param $width - * The new width of the resized image, in pixels. - * @param $height - * The new height of the resized image, in pixels. - * @return - * TRUE or FALSE, based on success. - * - * @see image_resize() - */ -function image_gd_resize(stdClass $image, $width, $height) { - $res = image_gd_create_tmp($image, $width, $height); - - if (!imagecopyresampled($res, $image->resource, 0, 0, 0, 0, $width, $height, $image->info['width'], $image->info['height'])) { - return FALSE; - } - - imagedestroy($image->resource); - // Update image object. - $image->resource = $res; - $image->info['width'] = $width; - $image->info['height'] = $height; - return TRUE; -} - -/** - * Rotate an image the given number of degrees. - * - * @param $image - * An image object. The $image->resource, $image->info['width'], and - * $image->info['height'] values will be modified by this call. - * @param $degrees - * The number of (clockwise) degrees to rotate the image. - * @param $background - * An hexadecimal integer specifying the background color to use for the - * uncovered area of the image after the rotation. E.g. 0x000000 for black, - * 0xff00ff for magenta, and 0xffffff for white. For images that support - * transparency, this will default to transparent. Otherwise it will - * be white. - * @return - * TRUE or FALSE, based on success. - * - * @see image_rotate() - */ -function image_gd_rotate(stdClass $image, $degrees, $background = NULL) { - // PHP installations using non-bundled GD do not have imagerotate. - if (!function_exists('imagerotate')) { - watchdog('image', 'The image %file could not be rotated because the imagerotate() function is not available in this PHP installation.', array('%file' => $image->source)); - return FALSE; - } - - $width = $image->info['width']; - $height = $image->info['height']; - - // Convert the hexadecimal background value to a color index value. - if (isset($background)) { - $rgb = array(); - for ($i = 16; $i >= 0; $i -= 8) { - $rgb[] = (($background >> $i) & 0xFF); - } - $background = imagecolorallocatealpha($image->resource, $rgb[0], $rgb[1], $rgb[2], 0); - } - // Set the background color as transparent if $background is NULL. - else { - // Get the current transparent color. - $background = imagecolortransparent($image->resource); - - // If no transparent colors, use white. - if ($background == 0) { - $background = imagecolorallocatealpha($image->resource, 255, 255, 255, 0); - } - } - - // Images are assigned a new color palette when rotating, removing any - // transparency flags. For GIF images, keep a record of the transparent color. - if ($image->info['extension'] == 'gif') { - $transparent_index = imagecolortransparent($image->resource); - if ($transparent_index != 0) { - $transparent_gif_color = imagecolorsforindex($image->resource, $transparent_index); - } - } - - $image->resource = imagerotate($image->resource, 360 - $degrees, $background); - - // GIFs need to reassign the transparent color after performing the rotate. - if (isset($transparent_gif_color)) { - $background = imagecolorexactalpha($image->resource, $transparent_gif_color['red'], $transparent_gif_color['green'], $transparent_gif_color['blue'], $transparent_gif_color['alpha']); - imagecolortransparent($image->resource, $background); - } - - $image->info['width'] = imagesx($image->resource); - $image->info['height'] = imagesy($image->resource); - return TRUE; -} - -/** - * Crop an image using the GD toolkit. - * - * @param $image - * An image object. The $image->resource, $image->info['width'], and - * $image->info['height'] values will be modified by this call. - * @param $x - * The starting x offset at which to start the crop, in pixels. - * @param $y - * The starting y offset at which to start the crop, in pixels. - * @param $width - * The width of the cropped area, in pixels. - * @param $height - * The height of the cropped area, in pixels. - * @return - * TRUE or FALSE, based on success. - * - * @see image_crop() - */ -function image_gd_crop(stdClass $image, $x, $y, $width, $height) { - $res = image_gd_create_tmp($image, $width, $height); - - if (!imagecopyresampled($res, $image->resource, 0, 0, $x, $y, $width, $height, $width, $height)) { - return FALSE; - } - - // Destroy the original image and return the modified image. - imagedestroy($image->resource); - $image->resource = $res; - $image->info['width'] = $width; - $image->info['height'] = $height; - return TRUE; -} - -/** - * Convert an image resource to grayscale. - * - * Note that transparent GIFs loose transparency when desaturated. - * - * @param $image - * An image object. The $image->resource value will be modified by this call. - * @return - * TRUE or FALSE, based on success. - * - * @see image_desaturate() - */ -function image_gd_desaturate(stdClass $image) { - // PHP installations using non-bundled GD do not have imagefilter. - if (!function_exists('imagefilter')) { - watchdog('image', 'The image %file could not be desaturated because the imagefilter() function is not available in this PHP installation.', array('%file' => $image->source)); - return FALSE; - } - - return imagefilter($image->resource, IMG_FILTER_GRAYSCALE); -} - -/** - * GD helper function to create an image resource from a file. - * - * @param $image - * An image object. The $image->resource value will populated by this call. - * @return - * TRUE or FALSE, based on success. - * - * @see image_load() - */ -function image_gd_load(stdClass $image) { - $extension = str_replace('jpg', 'jpeg', $image->info['extension']); - $function = 'imagecreatefrom' . $extension; - if (function_exists($function) && $image->resource = $function($image->source)) { - if (!imageistruecolor($image->resource)) { - // Convert indexed images to true color, so that filters work - // correctly and don't result in unnecessary dither. - $new_image = image_gd_create_tmp($image, $image->info['width'], $image->info['height']); - imagecopy($new_image, $image->resource, 0, 0, 0, 0, $image->info['width'], $image->info['height']); - imagedestroy($image->resource); - $image->resource = $new_image; - } - return (bool) $image->resource; - } - - return FALSE; -} - -/** - * GD helper to write an image resource to a destination file. - * - * @param $image - * An image object. - * @param $destination - * A string file URI or path where the image should be saved. - * @return - * TRUE or FALSE, based on success. - * - * @see image_save() - */ -function image_gd_save(stdClass $image, $destination) { - $scheme = file_uri_scheme($destination); - // Work around lack of stream wrapper support in imagejpeg() and imagepng(). - if ($scheme && file_stream_wrapper_valid_scheme($scheme)) { - // If destination is not local, save image to temporary local file. - $local_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL); - if (!isset($local_wrappers[$scheme])) { - $permanent_destination = $destination; - $destination = drupal_tempnam('temporary://', 'gd_'); - } - // Convert stream wrapper URI to normal path. - $destination = drupal_realpath($destination); - } - - $extension = str_replace('jpg', 'jpeg', $image->info['extension']); - $function = 'image' . $extension; - if (!function_exists($function)) { - return FALSE; - } - if ($extension == 'jpeg') { - $success = $function($image->resource, $destination, variable_get('image_jpeg_quality', 75)); - } - else { - // Always save PNG images with full transparency. - if ($extension == 'png') { - imagealphablending($image->resource, FALSE); - imagesavealpha($image->resource, TRUE); - } - $success = $function($image->resource, $destination); - } - // Move temporary local file to remote destination. - if (isset($permanent_destination) && $success) { - return (bool) file_unmanaged_move($destination, $permanent_destination, FILE_EXISTS_REPLACE); - } - return $success; -} - -/** - * Create a truecolor image preserving transparency from a provided image. - * - * @param $image - * An image object. - * @param $width - * The new width of the new image, in pixels. - * @param $height - * The new height of the new image, in pixels. - * @return - * A GD image handle. - */ -function image_gd_create_tmp(stdClass $image, $width, $height) { - $res = imagecreatetruecolor($width, $height); - - if ($image->info['extension'] == 'gif') { - // Grab transparent color index from image resource. - $transparent = imagecolortransparent($image->resource); - - if ($transparent >= 0) { - // The original must have a transparent color, allocate to the new image. - $transparent_color = imagecolorsforindex($image->resource, $transparent); - $transparent = imagecolorallocate($res, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); - - // Flood with our new transparent color. - imagefill($res, 0, 0, $transparent); - imagecolortransparent($res, $transparent); - } - } - elseif ($image->info['extension'] == 'png') { - imagealphablending($res, FALSE); - $transparency = imagecolorallocatealpha($res, 0, 0, 0, 127); - imagefill($res, 0, 0, $transparency); - imagealphablending($res, TRUE); - imagesavealpha($res, TRUE); - } - else { - imagefill($res, 0, 0, imagecolorallocate($res, 255, 255, 255)); - } - - return $res; -} - -/** - * Get details about an image. - * - * @param $image - * An image object. - * @return - * FALSE, if the file could not be found or is not an image. Otherwise, a - * keyed array containing information about the image: - * - "width": Width, in pixels. - * - "height": Height, in pixels. - * - "extension": Commonly used file extension for the image. - * - "mime_type": MIME type ('image/jpeg', 'image/gif', 'image/png'). - * - * @see image_get_info() - */ -function image_gd_get_info(stdClass $image) { - $details = FALSE; - $data = getimagesize($image->source); - - if (isset($data) && is_array($data)) { - $extensions = array('1' => 'gif', '2' => 'jpg', '3' => 'png'); - $extension = isset($extensions[$data[2]]) ? $extensions[$data[2]] : ''; - $details = array( - 'width' => $data[0], - 'height' => $data[1], - 'extension' => $extension, - 'mime_type' => $data['mime'], - ); - } - - return $details; -} - -/** - * @} End of "addtogroup image". - */ diff --git a/core/modules/system/lib/Drupal/system/Plugin/ImageToolkitInterface.php b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkitInterface.php new file mode 100644 index 0000000..2981f7a --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkitInterface.php @@ -0,0 +1,156 @@ +resource, $image->info['width'], and + * $image->info['height'] values will be modified by this call. + * @param $width + * The new width of the resized image, in pixels. + * @param $height + * The new height of the resized image, in pixels. + * @return + * TRUE or FALSE, based on success. + * + * @see image_resize() + */ + function resize(stdClass $image, $width, $height); + + /** + * Rotate an image the given number of degrees. + * + * @param $image + * An image object. The $image->resource, $image->info['width'], and + * $image->info['height'] values will be modified by this call. + * @param $degrees + * The number of (clockwise) degrees to rotate the image. + * @param $background + * An hexadecimal integer specifying the background color to use for the + * uncovered area of the image after the rotation. E.g. 0x000000 for black, + * 0xff00ff for magenta, and 0xffffff for white. For images that support + * transparency, this will default to transparent. Otherwise it will + * be white. + * @return + * TRUE or FALSE, based on success. + * + * @see image_rotate() + */ + function rotate(stdClass $image, $degrees, $background = NULL); + + /** + * Crop an image. + * + * @param $image + * An image object. The $image->resource, $image->info['width'], and + * $image->info['height'] values will be modified by this call. + * @param $x + * The starting x offset at which to start the crop, in pixels. + * @param $y + * The starting y offset at which to start the crop, in pixels. + * @param $width + * The width of the cropped area, in pixels. + * @param $height + * The height of the cropped area, in pixels. + * @return + * TRUE or FALSE, based on success. + * + * @see image_crop() + */ + function crop(stdClass $image, $x, $y, $width, $height); + + /** + * Convert an image resource to grayscale. + * + * Note that transparent GIFs loose transparency when desaturated. + * + * @param $image + * An image object. The $image->resource value will be modified by this call. + * @return + * TRUE or FALSE, based on success. + * + * @see image_desaturate() + */ + function desaturate(stdClass $image); + + /** + * Create an image resource from a file. + * + * @param $image + * An image object. The $image->resource value will populated by this call. + * @return + * TRUE or FALSE, based on success. + * + * @see image_load() + */ + function load(stdClass $image); + + /** + * Write an image resource to a destination file. + * + * @param $image + * An image object. + * @param $destination + * A string file URI or path where the image should be saved. + * @return + * TRUE or FALSE, based on success. + * + * @see image_save() + */ + function save(stdClass $image, $destination); + + /** + * Get details about an image. + * + * @param $image + * An image object. + * @return + * FALSE, if the file could not be found or is not an image. Otherwise, a + * keyed array containing information about the image: + * - "width": Width, in pixels. + * - "height": Height, in pixels. + * - "extension": Commonly used file extension for the image. + * - "mime_type": MIME type ('image/jpeg', 'image/gif', 'image/png'). + * + * @see image_get_info() + */ + function getInfo(stdClass $image); + + /** + * Verify Image Toolkit is set up correctly. + * + * @return + * A boolean indicating if the GD toolkit is available on this machine. + */ + static function checkAvailable(); +} diff --git a/core/modules/system/lib/Drupal/system/Plugin/ImageToolkitManager.php b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkitManager.php new file mode 100644 index 0000000..d801de0 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkitManager.php @@ -0,0 +1,24 @@ +discovery = new HookDiscovery('image_toolkits'); + $this->discovery = new AnnotatedClassDiscovery('system', 'imagetoolkit'); + $this->factory = new DefaultFactory($this->discovery); + } +} diff --git a/core/modules/system/lib/Drupal/system/Plugin/system/imagetoolkit/GDToolkit.php b/core/modules/system/lib/Drupal/system/Plugin/system/imagetoolkit/GDToolkit.php new file mode 100644 index 0000000..0a1b2af --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/system/imagetoolkit/GDToolkit.php @@ -0,0 +1,301 @@ +checkAvailable()) { + $form['status'] = array( + '#markup' => t('The GD toolkit is installed and working properly.') + ); + + $form['image_jpeg_quality'] = array( + '#type' => 'number', + '#title' => t('JPEG quality'), + '#description' => t('Define the image quality for JPEG manipulations. Ranges from 0 to 100. Higher values mean better image quality but bigger files.'), + '#min' => 0, + '#max' => 100, + '#default_value' => config('system.image.gd')->get('jpeg_quality'), + '#field_suffix' => t('%'), + ); + return $form; + } + else { + form_set_error('image_toolkit', t('The GD image toolkit requires that the GD module for PHP be installed and configured properly. For more information see PHP\'s image documentation.', array('@url' => 'http://php.net/image'))); + return FALSE; + } + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::settingsFormSubmit(). + */ + function settingsFormSubmit($form, &$form_state) { + config('system.image.gd') + ->set('jpeg_quality', $form_state['values']['gd']['image_jpeg_quality']) + ->save(); + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::resize(). + */ + function resize(stdClass $image, $width, $height) { + $res = $this->createTmp($image, $width, $height); + + if (!imagecopyresampled($res, $image->resource, 0, 0, 0, 0, $width, $height, $image->info['width'], $image->info['height'])) { + return FALSE; + } + + imagedestroy($image->resource); + // Update image object. + $image->resource = $res; + $image->info['width'] = $width; + $image->info['height'] = $height; + return TRUE; + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::rotate(). + */ + function rotate(stdClass $image, $degrees, $background = NULL) { + // PHP installations using non-bundled GD do not have imagerotate. + if (!function_exists('imagerotate')) { + watchdog('image', 'The image %file could not be rotated because the imagerotate() function is not available in this PHP installation.', array('%file' => $image->source)); + return FALSE; + } + + $width = $image->info['width']; + $height = $image->info['height']; + + // Convert the hexadecimal background value to a color index value. + if (isset($background)) { + $rgb = array(); + for ($i = 16; $i >= 0; $i -= 8) { + $rgb[] = (($background >> $i) & 0xFF); + } + $background = imagecolorallocatealpha($image->resource, $rgb[0], $rgb[1], $rgb[2], 0); + } + // Set the background color as transparent if $background is NULL. + else { + // Get the current transparent color. + $background = imagecolortransparent($image->resource); + + // If no transparent colors, use white. + if ($background == 0) { + $background = imagecolorallocatealpha($image->resource, 255, 255, 255, 0); + } + } + + // Images are assigned a new color palette when rotating, removing any + // transparency flags. For GIF images, keep a record of the transparent color. + if ($image->info['extension'] == 'gif') { + $transparent_index = imagecolortransparent($image->resource); + if ($transparent_index != 0) { + $transparent_gif_color = imagecolorsforindex($image->resource, $transparent_index); + } + } + + $image->resource = imagerotate($image->resource, 360 - $degrees, $background); + + // GIFs need to reassign the transparent color after performing the rotate. + if (isset($transparent_gif_color)) { + $background = imagecolorexactalpha($image->resource, $transparent_gif_color['red'], $transparent_gif_color['green'], $transparent_gif_color['blue'], $transparent_gif_color['alpha']); + imagecolortransparent($image->resource, $background); + } + + $image->info['width'] = imagesx($image->resource); + $image->info['height'] = imagesy($image->resource); + return TRUE; + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::crop(). + */ + function crop(stdClass $image, $x, $y, $width, $height) { + $res = $this->createTmp($image, $width, $height); + + if (!imagecopyresampled($res, $image->resource, 0, 0, $x, $y, $width, $height, $width, $height)) { + return FALSE; + } + + // Destroy the original image and return the modified image. + imagedestroy($image->resource); + $image->resource = $res; + $image->info['width'] = $width; + $image->info['height'] = $height; + return TRUE; + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::desaturate(). + */ + function desaturate(stdClass $image) { + // PHP installations using non-bundled GD do not have imagefilter. + if (!function_exists('imagefilter')) { + watchdog('image', 'The image %file could not be desaturated because the imagefilter() function is not available in this PHP installation.', array('%file' => $image->source)); + return FALSE; + } + + return imagefilter($image->resource, IMG_FILTER_GRAYSCALE); + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::load(). + */ + function load(stdClass $image) { + $extension = str_replace('jpg', 'jpeg', $image->info['extension']); + $function = 'imagecreatefrom' . $extension; + if (function_exists($function) && $image->resource = $function($image->source)) { + if (!imageistruecolor($image->resource)) { + // Convert indexed images to true color, so that filters work + // correctly and don't result in unnecessary dither. + $new_image = $this->createTmp($image, $image->info['width'], $image->info['height']); + imagecopy($new_image, $image->resource, 0, 0, 0, 0, $image->info['width'], $image->info['height']); + imagedestroy($image->resource); + $image->resource = $new_image; + } + return (bool) $image->resource; + } + + return FALSE; + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::save(). + */ + function save(stdClass $image, $destination) { + $scheme = file_uri_scheme($destination); + // Work around lack of stream wrapper support in imagejpeg() and imagepng(). + if ($scheme && file_stream_wrapper_valid_scheme($scheme)) { + // If destination is not local, save image to temporary local file. + $local_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL); + if (!isset($local_wrappers[$scheme])) { + $permanent_destination = $destination; + $destination = drupal_tempnam('temporary://', 'gd_'); + } + // Convert stream wrapper URI to normal path. + $destination = drupal_realpath($destination); + } + + $extension = str_replace('jpg', 'jpeg', $image->info['extension']); + $function = 'image' . $extension; + if (!function_exists($function)) { + return FALSE; + } + if ($extension == 'jpeg') { + $success = $function($image->resource, $destination, config('system.image.gd')->get('jpeg_quality')); + } + else { + // Always save PNG images with full transparency. + if ($extension == 'png') { + imagealphablending($image->resource, FALSE); + imagesavealpha($image->resource, TRUE); + } + $success = $function($image->resource, $destination); + } + // Move temporary local file to remote destination. + if (isset($permanent_destination) && $success) { + return (bool) file_unmanaged_move($destination, $permanent_destination, FILE_EXISTS_REPLACE); + } + return $success; + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::getInfo(). + */ + function getInfo(stdClass $image) { + $details = FALSE; + $data = getimagesize($image->source); + + if (isset($data) && is_array($data)) { + $extensions = array('1' => 'gif', '2' => 'jpg', '3' => 'png'); + $extension = isset($extensions[$data[2]]) ? $extensions[$data[2]] : ''; + $details = array( + 'width' => $data[0], + 'height' => $data[1], + 'extension' => $extension, + 'mime_type' => $data['mime'], + ); + } + + return $details; + } + + /** + * Create a truecolor image preserving transparency from a provided image. + * + * @param $image + * An image object. + * @param $width + * The new width of the new image, in pixels. + * @param $height + * The new height of the new image, in pixels. + * @return + * A GD image handle. + */ + function createTmp(stdClass $image, $width, $height) { + $res = imagecreatetruecolor($width, $height); + + if ($image->info['extension'] == 'gif') { + // Grab transparent color index from image resource. + $transparent = imagecolortransparent($image->resource); + + if ($transparent >= 0) { + // The original must have a transparent color, allocate to the new image. + $transparent_color = imagecolorsforindex($image->resource, $transparent); + $transparent = imagecolorallocate($res, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); + + // Flood with our new transparent color. + imagefill($res, 0, 0, $transparent); + imagecolortransparent($res, $transparent); + } + } + elseif ($image->info['extension'] == 'png') { + imagealphablending($res, FALSE); + $transparency = imagecolorallocatealpha($res, 0, 0, 0, 127); + imagefill($res, 0, 0, $transparency); + imagealphablending($res, TRUE); + imagesavealpha($res, TRUE); + } + else { + imagefill($res, 0, 0, imagecolorallocate($res, 255, 255, 255)); + } + + return $res; + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::checkAvailable(). + */ + public static function checkAvailable() { + if ($check = get_extension_funcs('gd')) { + if (in_array('imagegd2', $check)) { + // GD2 support is available. + return TRUE; + } + } + return FALSE; + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitGdTest.php b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitGdTest.php index 611126d..f03a1e5 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitGdTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitGdTest.php @@ -8,6 +8,7 @@ namespace Drupal\system\Tests\Image; use Drupal\simpletest\WebTestBase; +use Drupal\system\Plugin\ImageToolkitManager; /** * Test the core GD image manipulation functions. @@ -35,8 +36,9 @@ class ToolkitGdTest extends WebTestBase { } protected function checkRequirements() { - image_get_available_toolkits(); - if (!function_exists('image_gd_check_settings') || !image_gd_check_settings()) { + $manager = new ImageToolkitManager(); + $definition = $manager->getDefinition('gd'); + if (!call_user_func($definition['class'] . '::checkAvailable')) { return array( 'Image manipulations for the GD toolkit cannot run because the GD toolkit is not available.', ); @@ -204,7 +206,7 @@ class ToolkitGdTest extends WebTestBase { foreach ($files as $file) { foreach ($operations as $op => $values) { // Load up a fresh image. - $image = image_load(drupal_get_path('module', 'simpletest') . '/files/' . $file, 'gd'); + $image = image_load(drupal_get_path('module', 'simpletest') . '/files/' . $file, image_get_toolkit('gd')); if (!$image) { $this->fail(t('Could not load image %file.', array('%file' => $file))); continue 2; diff --git a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTest.php b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTest.php index 091fc5a..c70719a 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTest.php @@ -36,7 +36,7 @@ class ToolkitTest extends ToolkitTestBase { function testLoad() { $image = image_load($this->file, $this->toolkit); $this->assertTrue(is_object($image), t('Returned an object.')); - $this->assertEqual($this->toolkit, $image->toolkit, t('Image had toolkit set.')); + $this->assertEqual($this->toolkit, $image->toolkit); $this->assertToolkitOperationsCalled(array('load', 'get_info')); } diff --git a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTestBase.php index ac47080..d06938a 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTestBase.php +++ b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTestBase.php @@ -30,7 +30,7 @@ abstract class ToolkitTestBase extends WebTestBase { parent::setUp(); // Use the image_test.module's test toolkit. - $this->toolkit = 'test'; + $this->toolkit = image_get_toolkit('test'); // Pick a file for testing. $file = current($this->drupalGetTestFiles('image')); diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index 2bf4343..460cce1 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -5,6 +5,7 @@ * Admin page callbacks for the system module. */ +use Drupal\system\Plugin\ImageToolkitManager; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -1871,39 +1872,59 @@ function system_file_system_settings() { * Form builder; Configure site image toolkit usage. * * @ingroup forms - * @see system_settings_form() + * @see system_image_toolkit_settings_submit() */ -function system_image_toolkit_settings() { +function system_image_toolkit_settings($form, &$form_state) { + $config = config('system.image'); + $toolkits_available = image_get_available_toolkits(); - $current_toolkit = image_get_toolkit(); + $current_toolkit = config('system.image')->get('toolkit'); - if (count($toolkits_available) == 0) { - variable_del('image_toolkit'); - $form['image_toolkit_help'] = array( - '#markup' => t("No image toolkits were detected. Drupal includes support for PHP's built-in image processing functions but they were not detected on this system. You should consult your system administrator to have them enabled, or try using a third party toolkit.", array('gd-link' => url('http://php.net/gd'))), - ); - return $form; + $options = array(); + foreach($toolkits_available as $id => $definition) { + $options[$id] = $definition['title']; } - if (count($toolkits_available) > 1) { - $form['image_toolkit'] = array( - '#type' => 'radios', - '#title' => t('Select an image processing toolkit'), - '#default_value' => variable_get('image_toolkit', $current_toolkit), - '#options' => $toolkits_available + $form['image_toolkit'] = array( + '#type' => 'radios', + '#title' => t('Select an image processing toolkit'), + '#default_value' => $current_toolkit, + '#options' => $options, + ); + + // Get the toolkit settings forms. + $manager = new ImageToolkitManager(); + foreach ($toolkits_available as $id => $definition) { + $toolkit = $manager->createInstance($id); + $form['image_toolkit_settings'][$id] = array( + '#type' => 'fieldset', + '#title' => t('@toolkit settings', array('@toolkit' => $definition['title'])), + '#collapsible' => TRUE, + '#collapsed' => ($id == $current_toolkit) ? FALSE : TRUE, + '#tree' => TRUE, ); - } - else { - variable_set('image_toolkit', key($toolkits_available)); + $form['image_toolkit_settings'][$id] += $toolkit->settingsForm(); } - // Get the toolkit's settings form. - $function = 'image_' . $current_toolkit . '_settings'; - if (function_exists($function)) { - $form['image_toolkit_settings'] = $function(); - } + return system_config_form($form, $form_state); +} - return system_settings_form($form); +/** + * Form submission handler for system_image_toolkit_settings(). + */ +function system_image_toolkit_settings_submit($form, &$form_state) { + config('system.image') + ->set('toolkit', $form_state['values']['image_toolkit']) + ->save(); + + // Call the form submit handler for each of the toolkits. + // Get the toolkit settings forms. + $manager = new ImageToolkitManager(); + $toolkits_available = image_get_available_toolkits(); + foreach ($toolkits_available as $id => $definition) { + $toolkit = $manager->createInstance($id); + $toolkit->settingsFormSubmit($form, $form_state); + } } /** diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 486afba..4021e52 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1884,6 +1884,19 @@ function system_update_8019() { } /** + * Moves image toolkit settings from variable to config. + * + * @ingroup config_upgrade + */ +function system_update_8020() { + update_variables_to_config('system.image', array( + 'image_toolkit' => 'toolkit', + )); + update_variables_to_config('system.image.gd', array( + 'image_jpeg_quality' => 'jpeg_quality', + )); +} +/** * @} End of "defgroup updates-7.x-to-8.x". * The next series of updates should start at 9000. */ diff --git a/core/modules/system/system.module b/core/modules/system/system.module index f78f123..278bd4d 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -3836,19 +3836,6 @@ function theme_system_compact_link() { } /** - * Implements hook_image_toolkits(). - */ -function system_image_toolkits() { - include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'system') . '/' . 'image.gd.inc'; - return array( - 'gd' => array( - 'title' => t('GD2 image manipulation toolkit'), - 'available' => function_exists('image_gd_check_settings') && image_gd_check_settings(), - ), - ); -} - -/** * Attempts to get a file using drupal_http_request and to store it locally. * * @param $url diff --git a/core/modules/system/tests/modules/image_test/image_test.module b/core/modules/system/tests/modules/image_test/image_test.module index de640f0..c6291f2 100644 --- a/core/modules/system/tests/modules/image_test/image_test.module +++ b/core/modules/system/tests/modules/image_test/image_test.module @@ -6,22 +6,6 @@ */ /** - * Implements hook_image_toolkits(). - */ -function image_test_image_toolkits() { - return array( - 'test' => array( - 'title' => t('A dummy toolkit that works'), - 'available' => TRUE, - ), - 'broken' => array( - 'title' => t('A dummy toolkit that is "broken"'), - 'available' => FALSE, - ), - ); -} - -/** * Reset/initialize the history of calls to the toolkit functions. * * @see image_test_get_all_calls() diff --git a/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/system/imagetoolkit/BrokenToolkit.php b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/system/imagetoolkit/BrokenToolkit.php new file mode 100644 index 0000000..439191d --- /dev/null +++ b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/system/imagetoolkit/BrokenToolkit.php @@ -0,0 +1,29 @@ +_logCall('settings', array()); + return array(); + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::settingsFormSubmit(). + */ + function settingsFormSubmit($form, &$form_state) {} + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::getInfo(). + */ + function getInfo(stdClass $image) { + $this->_logCall('get_info', array($image)); + return array(); + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::load(). + */ + function load(stdClass $image) { + $this->_logCall('load', array($image)); + return $image; + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::save(). + */ + function save(stdClass $image, $destination) { + $this->_logCall('save', array($image, $destination)); + // Return false so that image_save() doesn't try to chmod the destination + // file that we didn't bother to create. + return FALSE; + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::crop(). + */ + function crop(stdClass $image, $x, $y, $width, $height) { + $this->_logCall('crop', array($image, $x, $y, $width, $height)); + return TRUE; + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::resize(). + */ + function resize(stdClass $image, $width, $height) { + $this->_logCall('resize', array($image, $width, $height)); + return TRUE; + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::rotate(). + */ + function rotate(stdClass $image, $degrees, $background = NULL) { + $this->_logCall('rotate', array($image, $degrees, $background)); + return TRUE; + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::desaturate(). + */ + function desaturate(stdClass $image) { + $this->_logCall('desaturate', array($image)); + return TRUE; + } + + /** + * Store the values passed to a toolkit call. + * + * @param $op + * One of the image toolkit operations: 'get_info', 'load', 'save', + * 'settings', 'resize', 'rotate', 'crop', 'desaturate'. + * @param $args + * Values passed to hook. + * + * @see image_test_get_all_calls() + * @see image_test_reset() + */ + function _logCall($op, $args) { + $results = variable_get('image_test_results', array()); + $results[$op][] = $args; + variable_set('image_test_results', $results); + } + + /** + * Implements Drupal\system\Plugin\ImageToolkitInterface::checkAvailable(). + */ + static function checkAvailable() { + return TRUE; + } +}