Commit c07e727d authored by catch's avatar catch

Issue #1664844 by alexpott, cweagans, heyrocker, eojthebrave, ACF, Berdir:...

Issue #1664844 by alexpott, cweagans, heyrocker, eojthebrave, ACF, Berdir: Convert image toolkits into plugins.
parent 54d4992f
This diff is collapsed.
......@@ -303,6 +303,16 @@ public function build(ContainerBuilder $container) {
$container->register('ajax.subscriber', 'Drupal\Core\Ajax\AjaxSubscriber')
->addTag('event_subscriber');
// Register image toolkit manager.
$container
->register('image.toolkit.manager', 'Drupal\system\Plugin\ImageToolkitManager')
->addArgument('%container.namespaces%');
// Register image toolkit.
$container
->register('image.toolkit', 'Drupal\system\Plugin\ImageToolkitInterface')
->setFactoryService('image.toolkit.manager')
->setFactoryMethod('getDefaultToolkit');
$container->addCompilerPass(new RegisterMatchersPass());
$container->addCompilerPass(new RegisterRouteFiltersPass());
// Add a compiler pass for registering event subscribers.
......
......@@ -79,7 +79,7 @@ function testFileValidateImageResolution() {
$this->assertEqual(count($errors), 1, t('Small images report an error.'), 'File');
// Maximum size.
if (image_get_toolkit()) {
if ($this->container->has('image.toolkit')) {
// Copy the image so that the original doesn't get resized.
copy('core/misc/druplicon.png', 'temporary://druplicon.png');
$this->image->uri = 'temporary://druplicon.png';
......
This diff is collapsed.
......@@ -20,7 +20,7 @@ class ImageEffectsTest extends ToolkitTestBase {
*
* @var array
*/
public static $modules = array('image_test', 'image_module_test');
public static $modules = array('image', 'image_test', 'image_module_test');
public static function getInfo() {
return array(
......@@ -44,7 +44,7 @@ function testResizeEffect() {
$this->assertToolkitOperationsCalled(array('resize'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['resize'][0][1], 1, 'Width was passed correctly');
$this->assertEqual($calls['resize'][0][2], 2, 'Height was passed correctly');
}
......@@ -58,7 +58,7 @@ function testScaleEffect() {
$this->assertToolkitOperationsCalled(array('resize'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['resize'][0][1], 10, 'Width was passed correctly');
$this->assertEqual($calls['resize'][0][2], 5, 'Height was based off aspect ratio and passed correctly');
}
......@@ -72,7 +72,7 @@ function testCropEffect() {
$this->assertToolkitOperationsCalled(array('crop'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['crop'][0][1], 0, 'X was passed correctly');
$this->assertEqual($calls['crop'][0][2], 1, 'Y was passed correctly');
$this->assertEqual($calls['crop'][0][3], 3, 'Width was passed correctly');
......@@ -87,7 +87,7 @@ function testScaleAndCropEffect() {
$this->assertToolkitOperationsCalled(array('resize', 'crop'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['crop'][0][1], 7.5, 'X was computed and passed correctly');
$this->assertEqual($calls['crop'][0][2], 0, 'Y was computed and passed correctly');
$this->assertEqual($calls['crop'][0][3], 5, 'Width was computed and passed correctly');
......@@ -102,7 +102,7 @@ function testDesaturateEffect() {
$this->assertToolkitOperationsCalled(array('desaturate'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual(count($calls['desaturate'][0]), 1, 'Only the image was passed.');
}
......@@ -115,7 +115,7 @@ function testRotateEffect() {
$this->assertToolkitOperationsCalled(array('rotate'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['rotate'][0][1], 90, 'Degrees were passed correctly');
$this->assertEqual($calls['rotate'][0][2], 0xffffff, 'Background color was passed correctly');
}
......
This diff is collapsed.
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkitInterface.
*/
namespace Drupal\system\Plugin;
/**
* Defines an interface for image toolkits.
*
* An image toolkit provides common image file manipulations like scaling,
* cropping, and rotating.
*/
interface ImageToolkitInterface {
/**
* Retrieves toolkit's settings form.
*
* @see system_image_toolkit_settings()
*/
function settingsForm();
/**
* Handles submissions for toolkit's settings form.
*
* @see system_image_toolkit_settings_submit()
*/
function settingsFormSubmit($form, &$form_state);
/**
* Scales an image to the specified size.
*
* @param object $image
* An image object. The $image->resource, $image->info['width'], and
* $image->info['height'] values will be modified by this call.
* @param int $width
* The new width of the resized image, in pixels.
* @param int $height
* The new height of the resized image, in pixels.
*
* @return bool
* TRUE or FALSE, based on success.
*
* @see image_resize()
*/
function resize($image, $width, $height);
/**
* Rotates an image the given number of degrees.
*
* @param object $image
* An image object. The $image->resource, $image->info['width'], and
* $image->info['height'] values will be modified by this call.
* @param int $degrees
* The number of (clockwise) degrees to rotate the image.
* @param string $background
* (optional) 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 bool
* TRUE or FALSE, based on success.
*
* @see image_rotate()
*/
function rotate($image, $degrees, $background = NULL);
/**
* Crops an image.
*
* @param object $image
* An image object. The $image->resource, $image->info['width'], and
* $image->info['height'] values will be modified by this call.
* @param int $x
* The starting x offset at which to start the crop, in pixels.
* @param int $y
* The starting y offset at which to start the crop, in pixels.
* @param int $width
* The width of the cropped area, in pixels.
* @param int $height
* The height of the cropped area, in pixels.
*
* @return bool
* TRUE or FALSE, based on success.
*
* @see image_crop()
*/
function crop($image, $x, $y, $width, $height);
/**
* Converts an image resource to grayscale.
*
* Note that transparent GIFs loose transparency when desaturated.
*
* @param object $image
* An image object. The $image->resource value will be modified by this
* call.
*
* @return bool
* TRUE or FALSE, based on success.
*
* @see image_desaturate()
*/
function desaturate($image);
/**
* Creates an image resource from a file.
*
* @param object $image
* An image object. The $image->resource value will populated by this call.
*
* @return bool
* TRUE or FALSE, based on success.
*
* @see image_load()
*/
function load($image);
/**
* Writes an image resource to a destination file.
*
* @param object $image
* An image object.
* @param string $destination
* A string file URI or path where the image should be saved.
*
* @return bool
* TRUE or FALSE, based on success.
*
* @see image_save()
*/
function save($image, $destination);
/**
* Gets details about an image.
*
* @param object $image
* An image object.
*
* @return array
* 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($image);
/**
* Verifies Image Toolkit is set up correctly.
*
* @return bool
* True if the GD toolkit is available on this machine.
*/
static function isAvailable();
}
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkitManager.
*/
namespace Drupal\system\Plugin;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
/**
* Manages toolkit plugins.
*/
class ImageToolkitManager extends PluginManagerBase {
/**
* Constructs the ImageToolkitManager object.
*/
public function __construct(array $namespaces) {
$this->discovery = new AnnotatedClassDiscovery('system', 'imagetoolkit', $namespaces);
$this->factory = new DefaultFactory($this->discovery);
}
/**
* Gets the default image toolkit.
*
* @param string $toolkit_id
* (optional) String specifying toolkit to load. NULL will load the default
* toolkit.
*
* @return \Drupal\system\Plugin\ImageToolkitInterface
* Object of the default toolkit, or FALSE on error.
*/
public function getDefaultToolkit() {
$toolkit_id = config('system.image')->get('toolkit');
$toolkits = $this->getAvailableToolkits();
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_id = key($toolkits);
}
if ($toolkit_id) {
$toolkit = $this->createInstance($toolkit_id);
}
else {
$toolkit = FALSE;
}
return $toolkit;
}
/**
* Gets a list of available toolkits.
*
* @return array
* An array with the toolkit names as keys and the descriptions as values.
*/
public function getAvailableToolkits() {
// Use plugin system to get list of available toolkits.
$toolkits = $this->getDefinitions();
$output = array();
foreach ($toolkits as $id => $definition) {
// Only allow modules that aren't marked as unavailable.
if (call_user_func($definition['class'] . '::isAvailable')) {
$output[$id] = $definition;
}
}
return $output;
}
}
......@@ -7,12 +7,13 @@
namespace Drupal\system\Tests\Image;
use Drupal\simpletest\WebTestBase;
use Drupal\simpletest\DrupalUnitTestBase;
use Drupal\system\Plugin\ImageToolkitManager;
/**
* Test the core GD image manipulation functions.
*/
class ToolkitGdTest extends WebTestBase {
class ToolkitGdTest extends DrupalUnitTestBase {
// Colors that are used in testing.
protected $black = array(0, 0, 0, 0);
protected $red = array(255, 0, 0, 0);
......@@ -26,6 +27,13 @@ class ToolkitGdTest extends WebTestBase {
protected $width = 40;
protected $height = 20;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'simpletest');
public static function getInfo() {
return array(
'name' => 'Image GD manipulation tests',
......@@ -35,8 +43,14 @@ public static function getInfo() {
}
protected function checkRequirements() {
image_get_available_toolkits();
if (!function_exists('image_gd_check_settings') || !image_gd_check_settings()) {
$gd_available = FALSE;
if ($check = get_extension_funcs('gd')) {
if (in_array('imagegd2', $check)) {
// GD2 support is available.
$gd_available = TRUE;
}
}
if (!$gd_available) {
return array(
'Image manipulations for the GD toolkit cannot run because the GD toolkit is not available.',
);
......@@ -201,10 +215,11 @@ function testManipulations() {
);
}
$manager = new ImageToolkitManager($this->container->getParameter('container.namespaces'));
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, $manager->createInstance('gd'));
if (!$image) {
$this->fail(t('Could not load image %file.', array('%file' => $file)));
continue 2;
......@@ -244,7 +259,7 @@ function testManipulations() {
$correct_dimensions_object = FALSE;
}
$directory = file_default_scheme() . '://imagetests';
$directory = $this->public_files_directory .'/imagetest';
file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
image_save($image, $directory . '/' . $op . '.' . $image->info['extension']);
......
......@@ -7,6 +7,8 @@
namespace Drupal\system\Tests\Image;
use Drupal\system\Plugin\ImageToolkitManager;
/**
* Test that the functions in image.inc correctly pass data to the toolkit.
*/
......@@ -20,11 +22,12 @@ public static function getInfo() {
}
/**
* Check that hook_image_toolkits() is called and only available toolkits are
* returned.
* Check that ImageToolkitManager::getAvailableToolkits() only returns
* available toolkits.
*/
function testGetAvailableToolkits() {
$toolkits = image_get_available_toolkits();
$manager = new ImageToolkitManager($this->container->getParameter('container.namespaces'));
$toolkits = $manager->getAvailableToolkits();
$this->assertTrue(isset($toolkits['test']), 'The working toolkit was returned.');
$this->assertFalse(isset($toolkits['broken']), 'The toolkit marked unavailable was not returned');
$this->assertToolkitOperationsCalled(array());
......@@ -36,7 +39,7 @@ function testGetAvailableToolkits() {
function testLoad() {
$image = image_load($this->file, $this->toolkit);
$this->assertTrue(is_object($image), 'Returned an object.');
$this->assertEqual($this->toolkit, $image->toolkit, 'Image had toolkit set.');
$this->assertEqual($this->toolkit, $image->toolkit, t('Image had toolkit set.'));
$this->assertToolkitOperationsCalled(array('load', 'get_info'));
}
......@@ -56,7 +59,7 @@ function testResize() {
$this->assertToolkitOperationsCalled(array('resize'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['resize'][0][1], 1, 'Width was passed correctly');
$this->assertEqual($calls['resize'][0][2], 2, 'Height was passed correctly');
}
......@@ -70,7 +73,7 @@ function testScale() {
$this->assertToolkitOperationsCalled(array('resize'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['resize'][0][1], 10, 'Width was passed correctly');
$this->assertEqual($calls['resize'][0][2], 5, 'Height was based off aspect ratio and passed correctly');
}
......@@ -83,7 +86,7 @@ function testScaleAndCrop() {
$this->assertToolkitOperationsCalled(array('resize', 'crop'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['crop'][0][1], 7.5, 'X was computed and passed correctly');
$this->assertEqual($calls['crop'][0][2], 0, 'Y was computed and passed correctly');
......@@ -99,7 +102,7 @@ function testRotate() {
$this->assertToolkitOperationsCalled(array('rotate'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['rotate'][0][1], 90, 'Degrees were passed correctly');
$this->assertEqual($calls['rotate'][0][2], 1, 'Background color was passed correctly');
}
......@@ -112,7 +115,7 @@ function testCrop() {
$this->assertToolkitOperationsCalled(array('crop'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual($calls['crop'][0][1], 1, 'X was passed correctly');
$this->assertEqual($calls['crop'][0][2], 2, 'Y was passed correctly');
$this->assertEqual($calls['crop'][0][3], 3, 'Width was passed correctly');
......@@ -127,7 +130,7 @@ function testDesaturate() {
$this->assertToolkitOperationsCalled(array('desaturate'));
// Check the parameters.
$calls = image_test_get_all_calls();
$calls = $this->imageTestGetAllCalls();
$this->assertEqual(count($calls['desaturate'][0]), 1, 'Only the image was passed.');
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\system\Tests\Image;
use Drupal\simpletest\WebTestBase;
use Drupal\system\Plugin\ImageToolkitManager;
use stdClass;
/**
......@@ -30,7 +31,8 @@ function setUp() {
parent::setUp();
// Use the image_test.module's test toolkit.
$this->toolkit = 'test';
$manager = new ImageToolkitManager($this->container->getParameter('container.namespaces'));
$this->toolkit = $manager->createInstance('test');
// Pick a file for testing.
$file = current($this->drupalGetTestFiles('image'));
......@@ -44,7 +46,7 @@ function setUp() {
$this->image->toolkit = $this->toolkit;
// Clear out any hook calls.
image_test_reset();
$this->imageTestReset();
}
/**
......@@ -57,7 +59,7 @@ function setUp() {
*/
function assertToolkitOperationsCalled(array $expected) {
// Determine which operations were called.
$actual = array_keys(array_filter(image_test_get_all_calls()));
$actual = array_keys(array_filter($this->imageTestGetAllCalls()));
// Determine if there were any expected that were not called.
$uncalled = array_diff($expected, $actual);
......@@ -77,4 +79,33 @@ function assertToolkitOperationsCalled(array $expected) {
$this->assertTrue(TRUE, 'No unexpected operations were called.');
}
}
/**
* Resets/initializes the history of calls to the test toolkit functions.
*/
function imageTestReset() {
// Keep track of calls to these operations
$results = array(
'load' => array(),
'save' => array(),
'settings' => array(),
'resize' => array(),
'rotate' => array(),
'crop' => array(),
'desaturate' => array(),
);
state()->set('image_test.results', $results);
}
/**
* Gets an array of calls to the test toolkit.
*
* @return array
* An array keyed by operation name ('load', 'save', 'settings', 'resize',
* 'rotate', 'crop', 'desaturate') with values being arrays of parameters
* passed to each call.
*/
function imageTestGetAllCalls() {
return state()->get('image_test.results') ?: array();
}
}
......@@ -1830,39 +1830,65 @@ function system_file_system_settings_submit($form, &$form_state) {
* Form builder; Configure site image toolkit usage.
*
* @ingroup forms
* @see system_settings_form()
* @see system_image_toolkit_settings_submit()
*/
function system_image_toolkit_settings() {
$toolkits_available = image_get_available_toolkits();
$current_toolkit = image_get_toolkit();
function system_image_toolkit_settings($form, &$form_state) {
$manager = Drupal::service('image.toolkit.manager');
$toolkits_available = $manager->getAvailableToolkits();
$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 <a href='!gd-link'>PHP's built-in image processing functions</a> 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;
}
// If we have available toolkits allow the user to select the image toolkit to
// use and load the settings forms.
if (count($toolkits_available)) {
$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
'#default_value' => $current_toolkit,
'#options' => $options,
);
// Get the toolkit settings forms.
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,
);
$form['image_toolkit_settings'][$id] += $toolkit->settingsForm();
}
$form