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 = system_config_form($form, $form_state);
}
else {
variable_set('image_toolkit', key($toolkits_available));
form_set_error('image_toolkit', t('There are no image toolkits available. Drupal comes with support for PHP\'s GD image toolkit. This requires that the GD module for PHP be installed and configured properly. For more information see <a href="@url">PHP\'s image documentation</a>.', array('@url' => 'http://php.net/image')));
}
// Get the toolkit's settings form.
$function = 'image_' . $current_toolkit . '_settings';
if (function_exists($function)) {
$form['image_toolkit_settings'] = $function();
}
return $form;
}
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 = Drupal::service('image.toolkit.manager');
foreach ($manager->getAvailableToolkits() as $id => $definition) {
$toolkit = $manager->createInstance($id);
$toolkit->settingsFormSubmit($form, $form_state);
}
}
/**
......
......@@ -1359,44 +1359,6 @@ function hook_forms($form_id, $args) {
function hook_init() {
}
/**
* Define image toolkits provided by this module.
*
* The file which includes each toolkit's functions must be declared as part of
* the files array in the module .info.yml file so that the registry will find
* and parse it.
*
* The toolkit's functions must be named image_toolkitname_operation().
* where the operation may be:
* - 'load': Required. See image_gd_load() for usage.
* - 'save': Required. See image_gd_save() for usage.
* - 'settings': Optional. See image_gd_settings() for usage.
* - 'resize': Optional. See image_gd_resize() for usage.
* - 'rotate': Optional. See image_gd_rotate() for usage.
* - 'crop': Optional. See image_gd_crop() for usage.
* - 'desaturate': Optional. See image_gd_desaturate() for usage.
*
* @return
* An array with the toolkit name as keys and sub-arrays with these keys:
* - 'title': A string with the toolkit's title.
* - 'available': A Boolean value to indicate that the toolkit is operating
* properly, e.g. all required libraries exist.
*
* @see system_image_toolkits()
*/
function hook_image_toolkits() {
return array(
'working' => array(
'title' => t('A toolkit that works.'),
'available' => TRUE,
),
'broken' => array(
'title' => t('A toolkit that is "broken" and will not be listed.'),
'available' => FALSE,
),
);
}
/**
* Alter an email message created with the drupal_mail() function.
*
......
......@@ -2052,7 +2052,7 @@ function system_update_8048() {
update_module_enable(array('menu_link'));
// Add the langcode column if it doesn't exist.
if (!db_field_exists('menu_links', 'langcode')) {
if (!db_field_exists('menu_inks', 'langcode')) {
$column = array(