From a48f0d889c399db882871558bc14a33b22220cfb Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Tue, 23 Jul 2013 18:14:41 +0200
Subject: [PATCH] Issue #1821854 by tim.plunkett, effulgentsia, larowlan:
 Convert image effects into plugins.

---
 .../Drupal/config/Tests/ConfigSchemaTest.php  |  12 +-
 .../image/config/image.style.large.yml        |   4 +-
 .../image/config/image.style.medium.yml       |   4 +-
 .../image/config/image.style.thumbnail.yml    |   4 +-
 .../image/config/schema/image.schema.yml      |   6 +-
 core/modules/image/image.admin.inc            | 349 +++---------------
 core/modules/image/image.api.php              |  50 +--
 core/modules/image/image.effects.inc          | 322 ----------------
 core/modules/image/image.install              |  11 +-
 core/modules/image/image.module               | 210 +----------
 core/modules/image/image.routing.yml          |  13 +
 core/modules/image/image.services.yml         |   3 +
 .../Drupal/image/Annotation/ImageEffect.php   |  48 +++
 .../ConfigurableImageEffectInterface.php      |  26 ++
 .../Drupal/image/Form/ImageEffectAddForm.php  |  69 ++++
 .../image/Form/ImageEffectDeleteForm.php      |  16 +-
 .../Drupal/image/Form/ImageEffectEditForm.php |  37 ++
 .../Drupal/image/Form/ImageEffectFormBase.php | 126 +++++++
 .../image/lib/Drupal/image/ImageEffectBag.php | 124 +++++++
 .../lib/Drupal/image/ImageEffectBase.php      | 112 ++++++
 .../lib/Drupal/image/ImageEffectInterface.php | 102 +++++
 .../lib/Drupal/image/ImageEffectManager.php   |  31 ++
 .../lib/Drupal/image/ImageStyleInterface.php  |  45 +++
 .../image/ImageStyleStorageController.php     |  36 --
 .../image/Plugin/Core/Entity/ImageStyle.php   |  92 +++--
 .../Plugin/ImageEffect/CropImageEffect.php    |  84 +++++
 .../ImageEffect/DesaturateImageEffect.php     |  42 +++
 .../Plugin/ImageEffect/ResizeImageEffect.php  |  79 ++++
 .../Plugin/ImageEffect/RotateImageEffect.php  | 132 +++++++
 .../ImageEffect/ScaleAndCropImageEffect.php   |  35 ++
 .../Plugin/ImageEffect/ScaleImageEffect.php   |  88 +++++
 .../image/Tests/ImageAdminStylesTest.php      |  51 +--
 .../image/Tests/ImageDimensionsTest.php       |  39 +-
 .../Drupal/image/Tests/ImageEffectsTest.php   |  65 +++-
 .../Drupal/image/Tests/ImageFieldTestBase.php |   4 -
 .../image/Tests/ImageStyleFlushTest.php       |   8 +-
 .../image/tests/image_module_test.module      |  51 ---
 .../image_module_test.info.yml                |   0
 .../image_module_test.module                  |  23 ++
 .../ImageEffect/NullTestImageEffect.php       |  31 ++
 .../Tests/Upgrade/ImageUpgradePathTest.php    |  14 +-
 41 files changed, 1533 insertions(+), 1065 deletions(-)
 delete mode 100644 core/modules/image/image.effects.inc
 create mode 100644 core/modules/image/lib/Drupal/image/Annotation/ImageEffect.php
 create mode 100644 core/modules/image/lib/Drupal/image/ConfigurableImageEffectInterface.php
 create mode 100644 core/modules/image/lib/Drupal/image/Form/ImageEffectAddForm.php
 create mode 100644 core/modules/image/lib/Drupal/image/Form/ImageEffectEditForm.php
 create mode 100644 core/modules/image/lib/Drupal/image/Form/ImageEffectFormBase.php
 create mode 100644 core/modules/image/lib/Drupal/image/ImageEffectBag.php
 create mode 100644 core/modules/image/lib/Drupal/image/ImageEffectBase.php
 create mode 100644 core/modules/image/lib/Drupal/image/ImageEffectInterface.php
 create mode 100644 core/modules/image/lib/Drupal/image/ImageEffectManager.php
 delete mode 100644 core/modules/image/lib/Drupal/image/ImageStyleStorageController.php
 create mode 100644 core/modules/image/lib/Drupal/image/Plugin/ImageEffect/CropImageEffect.php
 create mode 100644 core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php
 create mode 100644 core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ResizeImageEffect.php
 create mode 100644 core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php
 create mode 100644 core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleAndCropImageEffect.php
 create mode 100644 core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleImageEffect.php
 delete mode 100644 core/modules/image/tests/image_module_test.module
 rename core/modules/image/tests/{ => modules/image_module_test}/image_module_test.info.yml (100%)
 create mode 100644 core/modules/image/tests/modules/image_module_test/image_module_test.module
 create mode 100644 core/modules/image/tests/modules/image_module_test/lib/Drupal/image_module_test/Plugin/ImageEffect/NullTestImageEffect.php

diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigSchemaTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigSchemaTest.php
index d3bceb863b8c..f29320feb779 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigSchemaTest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigSchemaTest.php
@@ -104,10 +104,10 @@ function testSchemaMapping() {
     $expected['mapping']['label']['type'] = 'label';
     $expected['mapping']['effects']['type'] = 'sequence';
     $expected['mapping']['effects']['sequence'][0]['type'] = 'mapping';
-    $expected['mapping']['effects']['sequence'][0]['mapping']['name']['type'] = 'string';
-    $expected['mapping']['effects']['sequence'][0]['mapping']['data']['type'] = 'image.effect.[%parent.name]';
+    $expected['mapping']['effects']['sequence'][0]['mapping']['id']['type'] = 'string';
+    $expected['mapping']['effects']['sequence'][0]['mapping']['data']['type'] = 'image.effect.[%parent.id]';
     $expected['mapping']['effects']['sequence'][0]['mapping']['weight']['type'] = 'integer';
-    $expected['mapping']['effects']['sequence'][0]['mapping']['ieid']['type'] = 'string';
+    $expected['mapping']['effects']['sequence'][0]['mapping']['uuid']['type'] = 'string';
     $expected['mapping']['langcode']['label'] = 'Default language';
     $expected['mapping']['langcode']['type'] = 'string';
 
@@ -189,9 +189,9 @@ function testSchemaData() {
 
     // The function is_array() doesn't work with ArrayAccess, so we use count().
     $this->assertTrue(count($effects) == 1, 'Got an array with effects for image.style.large data');
-    $ieid = key($effects->getValue());
-    $effect = $effects[$ieid];
-    $this->assertTrue(count($effect['data']) && $effect['name']->getValue() == 'image_scale', 'Got data for the image scale effect from metadata.');
+    $uuid = key($effects->getValue());
+    $effect = $effects[$uuid];
+    $this->assertTrue(count($effect['data']) && $effect['id']->getValue() == 'image_scale', 'Got data for the image scale effect from metadata.');
     $this->assertEqual($effect['data']['width']->getType(), 'integer', 'Got the right type for the scale effect width.');
     $this->assertEqual($effect['data']['width']->getValue(), 480, 'Got the right value for the scale effect width.' );
 
diff --git a/core/modules/image/config/image.style.large.yml b/core/modules/image/config/image.style.large.yml
index 30bb1afe0a63..cbd1c0c9aac3 100644
--- a/core/modules/image/config/image.style.large.yml
+++ b/core/modules/image/config/image.style.large.yml
@@ -2,11 +2,11 @@ name: large
 label: 'Large (480x480)'
 effects:
   ddd73aa7-4bd6-4c85-b600-bdf2b1628d1d:
-    name: image_scale
+    id: image_scale
     data:
       width: '480'
       height: '480'
       upscale: '1'
     weight: '0'
-    ieid: ddd73aa7-4bd6-4c85-b600-bdf2b1628d1d
+    uuid: ddd73aa7-4bd6-4c85-b600-bdf2b1628d1d
 langcode: en
diff --git a/core/modules/image/config/image.style.medium.yml b/core/modules/image/config/image.style.medium.yml
index 1047f86043c6..ba38b73e5fdd 100644
--- a/core/modules/image/config/image.style.medium.yml
+++ b/core/modules/image/config/image.style.medium.yml
@@ -2,11 +2,11 @@ name: medium
 label: 'Medium (220x220)'
 effects:
   bddf0d06-42f9-4c75-a700-a33cafa25ea0:
-    name: image_scale
+    id: image_scale
     data:
       width: '220'
       height: '220'
       upscale: '1'
     weight: '0'
-    ieid: bddf0d06-42f9-4c75-a700-a33cafa25ea0
+    uuid: bddf0d06-42f9-4c75-a700-a33cafa25ea0
 langcode: en
diff --git a/core/modules/image/config/image.style.thumbnail.yml b/core/modules/image/config/image.style.thumbnail.yml
index 583481207ab6..0368dd36244e 100644
--- a/core/modules/image/config/image.style.thumbnail.yml
+++ b/core/modules/image/config/image.style.thumbnail.yml
@@ -2,11 +2,11 @@ name: thumbnail
 label: 'Thumbnail (100x100)'
 effects:
   1cfec298-8620-4749-b100-ccb6c4500779:
-    name: image_scale
+    id: image_scale
     data:
       width: '100'
       height: '100'
       upscale: '1'
     weight: '0'
-    ieid: 1cfec298-8620-4749-b100-ccb6c4500779
+    uuid: 1cfec298-8620-4749-b100-ccb6c4500779
 langcode: en
diff --git a/core/modules/image/config/schema/image.schema.yml b/core/modules/image/config/schema/image.schema.yml
index 6916f19b414b..871c0cc9e47f 100644
--- a/core/modules/image/config/schema/image.schema.yml
+++ b/core/modules/image/config/schema/image.schema.yml
@@ -26,13 +26,13 @@ image.style.*:
       sequence:
         - type: mapping
           mapping:
-            name:
+            id:
               type: string
             data:
-              type: image.effect.[%parent.name]
+              type: image.effect.[%parent.id]
             weight:
               type: integer
-            ieid:
+            uuid:
               type: string
     langcode:
       type: string
diff --git a/core/modules/image/image.admin.inc b/core/modules/image/image.admin.inc
index 1cd2155f1ebc..1e2646f51870 100644
--- a/core/modules/image/image.admin.inc
+++ b/core/modules/image/image.admin.inc
@@ -5,6 +5,9 @@
  * Administration pages for image settings.
  */
 
+use Drupal\Component\Utility\String;
+use Drupal\image\ConfigurableImageEffectInterface;
+use Drupal\image\ImageStyleInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 
 /**
@@ -35,7 +38,7 @@ function image_style_list() {
  * @ingroup forms
  * @see image_style_form_submit()
  */
-function image_style_form($form, &$form_state, $style) {
+function image_style_form($form, &$form_state, ImageStyleInterface $style) {
   $title = t('Edit style %name', array('%name' => $style->label()));
   drupal_set_title($title, PASS_THROUGH);
 
@@ -69,54 +72,56 @@ function image_style_form($form, &$form_state, $style) {
   $form['effects'] = array(
     '#theme' => 'image_style_effects',
   );
-  if (!empty($style->effects)) {
-    foreach ($style->effects as $key => $effect) {
-      $form['effects'][$key]['#weight'] = isset($form_state['input']['effects']) ? $form_state['input']['effects'][$key]['weight'] : NULL;
-      $form['effects'][$key]['label'] = array(
-        '#markup' => check_plain($effect['label']),
-      );
-      $form['effects'][$key]['summary'] = array(
-        '#markup' => isset($effect['summary theme']) ? theme($effect['summary theme'], array('data' => $effect['data'])) : '',
-      );
-      $form['effects'][$key]['weight'] = array(
-        '#type' => 'weight',
-        '#title' => t('Weight for @title', array('@title' => $effect['label'])),
-        '#title_display' => 'invisible',
-        '#default_value' => $effect['weight'],
-      );
+  foreach ($style->getEffects()->sort() as $effect) {
+    $key = $effect->getUuid();
+    $form['effects'][$key]['#weight'] = isset($form_state['input']['effects']) ? $form_state['input']['effects'][$key]['weight'] : NULL;
+    $form['effects'][$key]['label'] = array(
+      '#markup' => String::checkPlain($effect->label()),
+    );
+    $form['effects'][$key]['summary'] = $effect->getSummary();
+    $form['effects'][$key]['weight'] = array(
+      '#type' => 'weight',
+      '#title' => t('Weight for @title', array('@title' => $effect->label())),
+      '#title_display' => 'invisible',
+      '#default_value' => $effect->getWeight(),
+    );
 
-      $links = array();
-      if (isset($effect['form callback'])) {
-        $links['edit'] = array(
-          'title' => t('edit'),
-          'href' => 'admin/config/media/image-styles/manage/' . $style->id() . '/effects/' . $key,
-        );
-      }
-      $links['delete'] = array(
-        'title' => t('delete'),
-        'href' => 'admin/config/media/image-styles/manage/' . $style->id() . '/effects/' . $key . '/delete',
-      );
-      $form['effects'][$key]['operations'] = array(
-        '#type' => 'operations',
-        '#links' => $links,
-      );
-      $form['effects'][$key]['configure'] = array(
-        '#type' => 'link',
-        '#title' => t('edit'),
-        '#href' => 'admin/config/media/image-styles/manage/' . $style->id() . '/effects/' . $key,
-        '#access' => isset($effect['form callback']),
-      );
-      $form['effects'][$key]['remove'] = array(
-        '#type' => 'link',
-        '#title' => t('delete'),
-        '#href' => 'admin/config/media/image-styles/manage/' . $style->id() . '/effects/' . $key . '/delete',
+    $links = array();
+    $is_configurable = $effect instanceof ConfigurableImageEffectInterface;
+    if ($is_configurable) {
+      $links['edit'] = array(
+        'title' => t('edit'),
+        'href' => 'admin/config/media/image-styles/manage/' . $style->id() . '/effects/' . $key,
       );
     }
+    $links['delete'] = array(
+      'title' => t('delete'),
+      'href' => 'admin/config/media/image-styles/manage/' . $style->id() . '/effects/' . $key . '/delete',
+    );
+    $form['effects'][$key]['operations'] = array(
+      '#type' => 'operations',
+      '#links' => $links,
+    );
+    $form['effects'][$key]['configure'] = array(
+      '#type' => 'link',
+      '#title' => t('edit'),
+      '#href' => 'admin/config/media/image-styles/manage/' . $style->id() . '/effects/' . $key,
+      '#access' => $is_configurable,
+    );
+    $form['effects'][$key]['remove'] = array(
+      '#type' => 'link',
+      '#title' => t('delete'),
+      '#href' => 'admin/config/media/image-styles/manage/' . $style->id() . '/effects/' . $key . '/delete',
+    );
   }
 
   // Build the new image effect addition form and add it to the effect list.
   $new_effect_options = array();
-  foreach (image_effect_definitions() as $effect => $definition) {
+  $effects = Drupal::service('plugin.manager.image.effect')->getDefinitions();
+  uasort($effects, function ($a, $b) {
+    return strcasecmp($a['id'], $b['id']);
+  });
+  foreach ($effects as $effect => $definition) {
     $new_effect_options[$effect] = $definition['label'];
   }
   $form['effects']['new'] = array(
@@ -168,21 +173,21 @@ function image_style_form_add_validate($form, &$form_state) {
 function image_style_form_add_submit($form, &$form_state) {
   $style = $form_state['image_style'];
   // Check if this field has any configuration options.
-  $effect = image_effect_definition_load($form_state['values']['new']);
+  $effect = Drupal::service('plugin.manager.image.effect')->getDefinition($form_state['values']['new']);
 
   // Load the configuration form for this option.
-  if (isset($effect['form callback'])) {
+  if (is_subclass_of($effect['class'], '\Drupal\image\ConfigurableImageEffectInterface')) {
     $path = 'admin/config/media/image-styles/manage/' . $style->id() . '/add/' . $form_state['values']['new'];
     $form_state['redirect'] = array($path, array('query' => array('weight' => $form_state['values']['weight'])));
   }
   // If there's no form, immediately add the image effect.
   else {
     $effect = array(
-      'name' => $effect['name'],
+      'id' => $effect['id'],
       'data' => array(),
       'weight' => $form_state['values']['weight'],
     );
-    image_effect_save($style, $effect);
+    $style->saveImageEffect($effect);
     drupal_set_message(t('The image effect was successfully applied.'));
   }
 }
@@ -195,15 +200,9 @@ function image_style_form_submit($form, &$form_state) {
 
   // Update image effect weights.
   if (!empty($form_state['values']['effects'])) {
-    foreach ($form_state['values']['effects'] as $ieid => $effect_data) {
-      if (isset($style->effects[$ieid])) {
-        $effect = array(
-          'name' => $style->effects[$ieid]['name'],
-          'data' => $style->effects[$ieid]['data'],
-          'weight' => $effect_data['weight'],
-          'ieid' => $ieid,
-        );
-        $style->effects[$ieid] = $effect;
+    foreach ($form_state['values']['effects'] as $uuid => $effect_data) {
+      if ($style->getEffects()->has($uuid)) {
+        $style->getEffect($uuid)->setWeight($effect_data['weight']);
       }
     }
   }
@@ -261,246 +260,6 @@ function image_style_add_form_submit($form, &$form_state) {
   $form_state['redirect'] = 'admin/config/media/image-styles/manage/' . $style->id();
 }
 
-/**
- * Form builder; Form for adding and editing image effects.
- *
- * This form is used universally for editing all image effects. Each effect adds
- * its own custom section to the form by calling the 'form callback' specified
- * in hook_image_effect_info().
- *
- * @param $form_state
- *   An associative array containing the current state of the form.
- * @param $style
- *   An image style array.
- * @param $effect
- *   An image effect array.
- *
- * @ingroup forms
- * @see image_resize_form()
- * @see image_scale_form()
- * @see image_rotate_form()
- * @see image_crop_form()
- * @see image_effect_form_submit()
- */
-function image_effect_form($form, &$form_state, $style, $effect) {
-  // If there's no configuration for this image effect, return to
-  // the image style page.
-  if (!isset($effect['form callback'])) {
-    return new RedirectResponse(url('admin/config/media/image-styles/manage/' . $style->id(), array('absolute' => TRUE)));
-  }
-  $form_state['image_style'] = $style;
-  $form_state['image_effect'] = $effect;
-
-  if (!empty($effect['ieid'])) {
-    $title = t('Edit %label effect', array('%label' => $effect['label']));
-  }
-  else{
-    $title = t('Add %label effect', array('%label' => $effect['label']));
-  }
-  drupal_set_title($title, PASS_THROUGH);
-
-  $form['#attached']['css'][drupal_get_path('module', 'image') . '/css/image.admin.css'] = array();
-
-  $form['ieid'] = array(
-    '#type' => 'value',
-    '#value' => !empty($effect['ieid']) ? $effect['ieid'] : NULL,
-  );
-  $form['name'] = array(
-    '#type' => 'value',
-    '#value' => $effect['name'],
-  );
-
-  $form['data'] = call_user_func($effect['form callback'], $effect['data']);
-  $form['data']['#tree'] = TRUE;
-
-  // Check the URL for a weight, then the image effect, otherwise use default.
-  $weight = Drupal::request()->query->get('weight');
-  $form['weight'] = array(
-    '#type' => 'hidden',
-    '#value' => isset($weight) ? intval($weight) : (isset($effect['weight']) ? $effect['weight'] : count($style->effects)),
-  );
-
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => !empty($effect['ieid']) ? t('Update effect') : t('Add effect'),
-  );
-  $form['actions']['cancel'] = array(
-    '#type' => 'link',
-    '#title' => t('Cancel'),
-    '#href' => 'admin/config/media/image-styles/manage/' . $style->id(),
-  );
-
-  return $form;
-}
-
-/**
- * Submit handler for updating an image effect.
- */
-function image_effect_form_submit($form, &$form_state) {
-  form_state_values_clean($form_state);
-
-  $effect = $form_state['values'];
-  $style = $form_state['image_style'];
-  image_effect_save($style, $effect);
-
-  drupal_set_message(t('The image effect was successfully applied.'));
-  $form_state['redirect'] = 'admin/config/media/image-styles/manage/' . $style->id();
-}
-
-/**
- * Element validate handler to ensure a hexadecimal color value.
- */
-function image_effect_color_validate($element, &$form_state) {
-  if ($element['#value'] != '') {
-    $hex_value = preg_replace('/^#/', '', $element['#value']);
-    if (!preg_match('/^#[0-9A-F]{3}([0-9A-F]{3})?$/', $element['#value'])) {
-      form_error($element, t('!name must be a hexadecimal color value.', array('!name' => $element['#title'])));
-    }
-  }
-}
-
-/**
- * Element validate handler to ensure that either a height or a width is
- * specified.
- */
-function image_effect_scale_validate($element, &$form_state) {
-  if (empty($element['width']['#value']) && empty($element['height']['#value'])) {
-    form_error($element, t('Width and height can not both be blank.'));
-  }
-}
-
-/**
- * Form structure for the image resize form.
- *
- * Note that this is not a complete form, it only contains the portion of the
- * form for configuring the resize options. Therefore it does not not need to
- * include metadata about the effect, nor a submit button.
- *
- * @param $data
- *   The current configuration for this resize effect.
- */
-function image_resize_form($data) {
-  $form['width'] = array(
-    '#type' => 'number',
-    '#title' => t('Width'),
-    '#default_value' => isset($data['width']) ? $data['width'] : '',
-    '#field_suffix' => ' ' . t('pixels'),
-    '#required' => TRUE,
-    '#min' => 1,
-  );
-  $form['height'] = array(
-    '#type' => 'number',
-    '#title' => t('Height'),
-    '#default_value' => isset($data['height']) ? $data['height'] : '',
-    '#field_suffix' => ' ' . t('pixels'),
-    '#required' => TRUE,
-    '#min' => 1,
-  );
-  return $form;
-}
-
-/**
- * Form structure for the image scale form.
- *
- * Note that this is not a complete form, it only contains the portion of the
- * form for configuring the scale options. Therefore it does not not need to
- * include metadata about the effect, nor a submit button.
- *
- * @param $data
- *   The current configuration for this scale effect.
- */
-function image_scale_form($data) {
-  $form = image_resize_form($data);
-  $form['#element_validate'] = array('image_effect_scale_validate');
-  $form['width']['#required'] = FALSE;
-  $form['height']['#required'] = FALSE;
-  $form['upscale'] = array(
-    '#type' => 'checkbox',
-    '#default_value' => (isset($data['upscale'])) ? $data['upscale'] : 0,
-    '#title' => t('Allow Upscaling'),
-    '#description' => t('Let scale make images larger than their original size'),
-  );
-  return $form;
-}
-
-/**
- * Form structure for the image crop form.
- *
- * Note that this is not a complete form, it only contains the portion of the
- * form for configuring the crop options. Therefore it does not not need to
- * include metadata about the effect, nor a submit button.
- *
- * @param $data
- *   The current configuration for this crop effect.
- */
-function image_crop_form($data) {
-  $data += array(
-    'width' => '',
-    'height' => '',
-    'anchor' => 'center-center',
-  );
-
-  $form = image_resize_form($data);
-  $form['anchor'] = array(
-    '#type' => 'radios',
-    '#title' => t('Anchor'),
-    '#options' => array(
-      'left-top'      => t('Top') . ' ' . t('Left'),
-      'center-top'    => t('Top') . ' ' . t('Center'),
-      'right-top'     => t('Top') . ' ' . t('Right'),
-      'left-center'   => t('Center') . ' ' . t('Left'),
-      'center-center' => t('Center'),
-      'right-center'  => t('Center') . ' ' . t('Right'),
-      'left-bottom'   => t('Bottom') . ' ' . t('Left'),
-      'center-bottom' => t('Bottom') . ' ' . t('Center'),
-      'right-bottom'  => t('Bottom') . ' ' . t('Right'),
-    ),
-    '#theme' => 'image_anchor',
-    '#default_value' => $data['anchor'],
-    '#description' => t('The part of the image that will be retained during the crop.'),
-  );
-
-  return $form;
-}
-
-/**
- * Form structure for the image rotate form.
- *
- * Note that this is not a complete form, it only contains the portion of the
- * form for configuring the rotate options. Therefore it does not not need to
- * include metadata about the effect, nor a submit button.
- *
- * @param $data
- *   The current configuration for this rotate effect.
- */
-function image_rotate_form($data) {
-  $form['degrees'] = array(
-    '#type' => 'number',
-    '#default_value' => (isset($data['degrees'])) ? $data['degrees'] : 0,
-    '#title' => t('Rotation angle'),
-    '#description' => t('The number of degrees the image should be rotated. Positive numbers are clockwise, negative are counter-clockwise.'),
-    '#field_suffix' => '&deg;',
-    '#required' => TRUE,
-  );
-  $form['bgcolor'] = array(
-    '#type' => 'textfield',
-    '#default_value' => (isset($data['bgcolor'])) ? $data['bgcolor'] : '#FFFFFF',
-    '#title' => t('Background color'),
-    '#description' => t('The background color to use for exposed areas of the image. Use web-style hex colors (#FFFFFF for white, #000000 for black). Leave blank for transparency on image types that support it.'),
-    '#size' => 7,
-    '#maxlength' => 7,
-    '#element_validate' => array('image_effect_color_validate'),
-  );
-  $form['random'] = array(
-    '#type' => 'checkbox',
-    '#default_value' => (isset($data['random'])) ? $data['random'] : 0,
-    '#title' => t('Randomize'),
-    '#description' => t('Randomize the rotation angle for each image. The angle specified above is used as a maximum.'),
-  );
-  return $form;
-}
-
 /**
  * Returns HTML for the page containing the list of image styles.
  *
diff --git a/core/modules/image/image.api.php b/core/modules/image/image.api.php
index bc7ace8cc99b..7678142e5d53 100644
--- a/core/modules/image/image.api.php
+++ b/core/modules/image/image.api.php
@@ -11,58 +11,14 @@
  */
 
 /**
- * Define information about image effects provided by a module.
- *
- * This hook enables modules to define image manipulation effects for use with
- * an image style.
- *
- * @return
- *   An array of image effects. This array is keyed on the machine-readable
- *   effect name. Each effect is defined as an associative array containing the
- *   following items:
- *   - "label": The human-readable name of the effect.
- *   - "effect callback": The function to call to perform this image effect.
- *   - "dimensions passthrough": (optional) Set this item if the effect doesn't
- *     change the dimensions of the image.
- *   - "dimensions callback": (optional) The function to call to transform
- *     dimensions for this effect.
- *   - "help": (optional) A brief description of the effect that will be shown
- *     when adding or configuring this image effect.
- *   - "form callback": (optional) The name of a function that will return a
- *     $form array providing a configuration form for this image effect.
- *   - "summary theme": (optional) The name of a theme function that will output
- *     a summary of this image effect's configuration.
- *
- * @see hook_image_effect_info_alter()
- */
-function hook_image_effect_info() {
-  $effects = array();
-
-  $effects['mymodule_resize'] = array(
-    'label' => t('Resize'),
-    'help' => t('Resize an image to an exact set of dimensions, ignoring aspect ratio.'),
-    'effect callback' => 'mymodule_resize_effect',
-    'dimensions callback' => 'mymodule_resize_dimensions',
-    'form callback' => 'mymodule_resize_form',
-    'summary theme' => 'mymodule_resize_summary',
-  );
-
-  return $effects;
-}
-
-/**
- * Alter the information provided in hook_image_effect_info().
+ * Alter the information provided in \Drupal\image\Annotation\ImageEffect.
  *
  * @param $effects
  *   The array of image effects, keyed on the machine-readable effect name.
- *
- * @see hook_image_effect_info()
  */
 function hook_image_effect_info_alter(&$effects) {
-  // Override the Image module's crop effect with more options.
-  $effects['image_crop']['effect callback'] = 'mymodule_crop_effect';
-  $effects['image_crop']['dimensions callback'] = 'mymodule_crop_dimensions';
-  $effects['image_crop']['form callback'] = 'mymodule_crop_form';
+  // Override the Image module's 'Scale and Crop' effect label.
+  $effects['image_scale_and_crop']['label'] = t('Bangers and Mash');
 }
 
 /**
diff --git a/core/modules/image/image.effects.inc b/core/modules/image/image.effects.inc
deleted file mode 100644
index 619b09b0760d..000000000000
--- a/core/modules/image/image.effects.inc
+++ /dev/null
@@ -1,322 +0,0 @@
-<?php
-
-/**
- * @file
- * Functions needed to execute image effects provided by Image module.
- */
-
-/**
- * Implements hook_image_effect_info().
- */
-function image_image_effect_info() {
-  $effects = array(
-    'image_resize' => array(
-      'label' => t('Resize'),
-      'help' => t('Resizing will make images an exact set of dimensions. This may cause images to be stretched or shrunk disproportionately.'),
-      'effect callback' => 'image_resize_effect',
-      'dimensions callback' => 'image_resize_dimensions',
-      'form callback' => 'image_resize_form',
-      'summary theme' => 'image_resize_summary',
-    ),
-    'image_scale' => array(
-      'label' => t('Scale'),
-      'help' => t('Scaling will maintain the aspect-ratio of the original image. If only a single dimension is specified, the other dimension will be calculated.'),
-      'effect callback' => 'image_scale_effect',
-      'dimensions callback' => 'image_scale_dimensions',
-      'form callback' => 'image_scale_form',
-      'summary theme' => 'image_scale_summary',
-    ),
-    'image_scale_and_crop' => array(
-      'label' => t('Scale and crop'),
-      'help' => t('Scale and crop will maintain the aspect-ratio of the original image, then crop the larger dimension. This is most useful for creating perfectly square thumbnails without stretching the image.'),
-      'effect callback' => 'image_scale_and_crop_effect',
-      'dimensions callback' => 'image_resize_dimensions',
-      'form callback' => 'image_resize_form',
-      'summary theme' => 'image_resize_summary',
-    ),
-    'image_crop' => array(
-      'label' => t('Crop'),
-      'help' => t('Cropping will remove portions of an image to make it the specified dimensions.'),
-      'effect callback' => 'image_crop_effect',
-      'dimensions callback' => 'image_resize_dimensions',
-      'form callback' => 'image_crop_form',
-      'summary theme' => 'image_crop_summary',
-    ),
-    'image_desaturate' => array(
-      'label' => t('Desaturate'),
-      'help' => t('Desaturate converts an image to grayscale.'),
-      'effect callback' => 'image_desaturate_effect',
-      'dimensions passthrough' => TRUE,
-    ),
-    'image_rotate' => array(
-      'label' => t('Rotate'),
-      'help' => t('Rotating an image may cause the dimensions of an image to increase to fit the diagonal.'),
-      'effect callback' => 'image_rotate_effect',
-      'dimensions callback' => 'image_rotate_dimensions',
-      'form callback' => 'image_rotate_form',
-      'summary theme' => 'image_rotate_summary',
-    ),
-  );
-
-  return $effects;
-}
-
-/**
- * Image effect callback; Resize an image resource.
- *
- * @param object $image
- *   An image object returned by image_load().
- * @param array $data
- *   An array of attributes to use when performing the resize effect with the
- *   following items:
- *   - "width": An integer representing the desired width in pixels.
- *   - "height": An integer representing the desired height in pixels.
- *
- * @return bool
- *   TRUE on success. FALSE on failure to resize image.
- *
- * @see image_resize()
- */
-function image_resize_effect($image, array $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->getPluginId(), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
-    return FALSE;
-  }
-  return TRUE;
-}
-
-/**
- * Image dimensions callback; Resize.
- *
- * @param array $dimensions
- *   Dimensions to be modified - an array with components width and height, in
- *   pixels.
- * @param array $data
- *   An array of attributes to use when performing the resize effect with the
- *   following items:
- *   - "width": An integer representing the desired width in pixels.
- *   - "height": An integer representing the desired height in pixels.
- */
-function image_resize_dimensions(array &$dimensions, array $data) {
-  // The new image will have the exact dimensions defined for the effect.
-  $dimensions['width'] = $data['width'];
-  $dimensions['height'] = $data['height'];
-}
-
-/**
- * Image effect callback; Scale an image resource.
- *
- * @param object $image
- *   An image object returned by image_load().
- * @param array $data
- *   An array of attributes to use when performing the scale effect with the
- *   following items:
- *   - "width": An integer representing the desired width in pixels.
- *   - "height": An integer representing the desired height in pixels.
- *   - "upscale": A boolean indicating that the image should be upscaled if the
- *     dimensions are larger than the original image.
- *
- * @return bool
- *   TRUE on success. FALSE on failure to scale image.
- *
- * @see image_scale()
- */
-function image_scale_effect($image, array $data) {
-  // Set sane default values.
-  $data += array(
-    'width' => NULL,
-    'height' => NULL,
-    'upscale' => FALSE,
-  );
-
-  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->getPluginId(), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
-    return FALSE;
-  }
-  return TRUE;
-}
-
-/**
- * Image dimensions callback; Scale.
- *
- * @param array $dimensions
- *   Dimensions to be modified - an array with components width and height, in
- *   pixels.
- * @param array $data
- *   An array of attributes to use when performing the scale effect with the
- *   following items:
- *   - "width": An integer representing the desired width in pixels.
- *   - "height": An integer representing the desired height in pixels.
- *   - "upscale": A boolean indicating that the image should be upscaled if the
- *     dimensions are larger than the original image.
- */
-function image_scale_dimensions(array &$dimensions, array $data) {
-  if ($dimensions['width'] && $dimensions['height']) {
-    image_dimensions_scale($dimensions, $data['width'], $data['height'], $data['upscale']);
-  }
-}
-
-/**
- * Image effect callback; Crop an image resource.
- *
- * @param object $image
- *   An image object returned by image_load().
- * @param array $data
- *   An array of attributes to use when performing the crop effect with the
- *   following items:
- *   - "width": An integer representing the desired width in pixels.
- *   - "height": An integer representing the desired height in pixels.
- *   - "anchor": A string describing where the crop should originate in the form
- *     of "XOFFSET-YOFFSET". XOFFSET is either a number of pixels or
- *     "left", "center", "right" and YOFFSET is either a number of pixels or
- *     "top", "center", "bottom".
- *
- * @return bool
- *   TRUE on success. FALSE on failure to crop image.
- *
- * @see image_crop()
- */
-function image_crop_effect($image, array $data) {
-  // Set sane default values.
-  $data += array(
-    'anchor' => 'center-center',
-  );
-
-  list($x, $y) = explode('-', $data['anchor']);
-  $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->getPluginId(), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
-    return FALSE;
-  }
-  return TRUE;
-}
-
-/**
- * Image effect callback; Scale and crop an image resource.
- *
- * @param object $image
- *   An image object returned by image_load().
- * @param array $data
- *   An array of attributes to use when performing the scale and crop effect
- *   with the following items:
- *   - "width": An integer representing the desired width in pixels.
- *   - "height": An integer representing the desired height in pixels.
- *
- * @return bool
- *   TRUE on success. FALSE on failure to scale and crop image.
- *
- * @see image_scale_and_crop()
- */
-function image_scale_and_crop_effect($image, array $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->getPluginId(), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
-    return FALSE;
-  }
-  return TRUE;
-}
-
-/**
- * Image effect callback; Desaturate (grayscale) an image resource.
- *
- * @param object $image
- *   An image object returned by image_load().
- * @param array $data
- *   An array of attributes to use when performing the desaturate effect.
- *
- * @return bool
- *   TRUE on success. FALSE on failure to desaturate image.
- *
- * @see image_desaturate()
- */
-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->getPluginId(), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
-    return FALSE;
-  }
-  return TRUE;
-}
-
-/**
- * Image effect callback; Rotate an image resource.
- *
- * @param object $image
- *   An image object returned by image_load().
- * @param array $data
- *   An array of attributes to use when performing the rotate effect containing
- *   the following items:
- *   - "degrees": The number of (clockwise) degrees to rotate the image.
- *   - "random": A boolean indicating that a random rotation angle should be
- *     used for this image. The angle specified in "degrees" is used as a
- *     positive and negative maximum.
- *   - "bgcolor": The background color to use for exposed areas of the image.
- *     Use web-style hex colors (#FFFFFF for white, #000000 for black). Leave
- *     blank for transparency on image types that support it.
- *
- * @return bool
- *   TRUE on success. FALSE on failure to rotate image.
- *
- * @see image_rotate().
- */
-function image_rotate_effect($image, $data) {
-  // Set sane default values.
-  $data += array(
-    'degrees' => 0,
-    'bgcolor' => NULL,
-    'random' => FALSE,
-  );
-
-  // Convert short #FFF syntax to full #FFFFFF syntax.
-  if (strlen($data['bgcolor']) == 4) {
-    $c = $data['bgcolor'];
-    $data['bgcolor'] = $c[0] . $c[1] . $c[1] . $c[2] . $c[2] . $c[3] . $c[3];
-  }
-
-  // Convert #FFFFFF syntax to hexadecimal colors.
-  if ($data['bgcolor'] != '') {
-    $data['bgcolor'] = hexdec(str_replace('#', '0x', $data['bgcolor']));
-  }
-  else {
-    $data['bgcolor'] = NULL;
-  }
-
-  if (!empty($data['random'])) {
-    $degrees = abs((float) $data['degrees']);
-    $data['degrees'] = rand(-1 * $degrees, $degrees);
-  }
-
-  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->getPluginId(), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
-    return FALSE;
-  }
-  return TRUE;
-}
-
-/**
- * Image dimensions callback; Rotate.
- *
- * @param array $dimensions
- *   Dimensions to be modified - an array with components width and height, in
- *   pixels.
- * @param array $data
- *   An array of attributes to use when performing the rotate effect containing
- *   the following items:
- *   - "degrees": The number of (clockwise) degrees to rotate the image.
- *   - "random": A boolean indicating that a random rotation angle should be
- *     used for this image. The angle specified in "degrees" is used as a
- *     positive and negative maximum.
- */
-function image_rotate_dimensions(array &$dimensions, array $data) {
-  // If the rotate is not random and the angle is a multiple of 90 degrees,
-  // then the new dimensions can be determined.
-  if (!$data['random'] && ((int) ($data['degrees']) == $data['degrees']) && ($data['degrees'] % 90 == 0)) {
-    if ($data['degrees'] % 180 != 0) {
-      $temp = $dimensions['width'];
-      $dimensions['width'] = $dimensions['height'];
-      $dimensions['height'] = $temp;
-    }
-  }
-  else {
-    $dimensions['width'] = $dimensions['height'] = NULL;
-  }
-}
diff --git a/core/modules/image/image.install b/core/modules/image/image.install
index fd440da7b306..1617e8c5d752 100644
--- a/core/modules/image/image.install
+++ b/core/modules/image/image.install
@@ -126,14 +126,19 @@ function _image_update_get_style_with_effects(array $style) {
     ->condition('isid', $style['isid'])
     ->execute();
   foreach ($result as $effect) {
-    unset($effect['isid']);
     $effect['data'] = unserialize($effect['data']);
 
     // Generate a unique image effect ID for the effect.
     $uuid = new Uuid();
-    $effect['ieid'] = $uuid->generate();
+    $effect['uuid'] = $uuid->generate();
 
-    $effects[$effect['ieid']] = $effect;
+    // Use 'id' instead of 'name'.
+    $effect['id'] = $effect['name'];
+
+    // Clear out legacy keys.
+    unset($effect['isid'], $effect['ieid'], $effect['name']);
+
+    $effects[$effect['uuid']] = $effect;
   }
   return $effects;
 }
diff --git a/core/modules/image/image.module b/core/modules/image/image.module
index 62aee359fbd6..fefbf00ebb7c 100644
--- a/core/modules/image/image.module
+++ b/core/modules/image/image.module
@@ -6,10 +6,8 @@
  */
 
 use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Language\Language;
 use Drupal\field\Plugin\Core\Entity\Field;
 use Drupal\field\Plugin\Core\Entity\FieldInstance;
-use Drupal\Component\Uuid\Uuid;
 use Drupal\file\Plugin\Core\Entity\File;
 use Drupal\image\ImageStyleInterface;
 use Drupal\image\Plugin\Core\Entity\ImageStyle;
@@ -72,11 +70,11 @@ function image_help($path, $arg) {
     case 'admin/config/media/image-styles':
       return '<p>' . t('Image styles commonly provide thumbnail sizes by scaling and cropping images, but can also add various effects before an image is displayed. When an image is displayed with a style, a new file is created and the original image is left unchanged.') . '</p>';
     case 'admin/config/media/image-styles/manage/%/add/%':
-      $effect = image_effect_definition_load($arg[7]);
-      return isset($effect['help']) ? ('<p>' . $effect['help'] . '</p>') : NULL;
+      $effect = Drupal::service('plugin.manager.image.effect')->getDefinition($arg[7]);
+      return isset($effect['description']) ? ('<p>' . $effect['description'] . '</p>') : NULL;
     case 'admin/config/media/image-styles/manage/%/effects/%':
-      $effect = ($arg[5] == 'add') ? image_effect_definition_load($arg[6]) : image_effect_load($arg[6], $arg[4]);
-      return isset($effect['help']) ? ('<p>' . $effect['help'] . '</p>') : NULL;
+      $effect = entity_load('image_style', $arg[5])->getEffect($arg[7])->getPluginDefinition();
+      return isset($effect['description']) ? ('<p>' . $effect['description'] . '</p>') : NULL;
   }
 }
 
@@ -142,28 +140,20 @@ function image_menu() {
     'weight' => 10,
     'route_name' => 'image_style_delete',
   );
-  $items['admin/config/media/image-styles/manage/%image_style/effects/%image_effect'] = array(
+  $items['admin/config/media/image-styles/manage/%/effects/%'] = array(
     'title' => 'Edit image effect',
     'description' => 'Edit an existing effect within a style.',
-    'load arguments' => array(5, (string) IMAGE_STORAGE_EDITABLE),
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('image_effect_form', 5, 7),
-    'access arguments' => array('administer image styles'),
-    'file' => 'image.admin.inc',
+    'route_name' => 'image_effect_edit_form',
   );
-  $items['admin/config/media/image-styles/manage/%image_style/effects/%image_effect/delete'] = array(
+  $items['admin/config/media/image-styles/manage/%image_style/effects/%/delete'] = array(
     'title' => 'Delete image effect',
     'description' => 'Delete an existing effect from a style.',
     'route_name' => 'image_effect_delete',
   );
-  $items['admin/config/media/image-styles/manage/%image_style/add/%image_effect_definition'] = array(
+  $items['admin/config/media/image-styles/manage/%/add/%'] = array(
     'title' => 'Add image effect',
     'description' => 'Add a new effect to a style.',
-    'load arguments' => array(5),
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('image_effect_form', 5, 7),
-    'access arguments' => array('administer image styles'),
-    'file' => 'image.admin.inc',
+    'route_name' => 'image_effect_add_form',
   );
 
   return $items;
@@ -190,35 +180,45 @@ function image_theme() {
     // Theme functions in image.admin.inc.
     'image_style_list' => array(
       'variables' => array('styles' => NULL),
+      'file' => 'image.admin.inc',
     ),
     'image_style_effects' => array(
       'render element' => 'form',
+      'file' => 'image.admin.inc',
     ),
     'image_style_preview' => array(
       'variables' => array('style' => NULL),
+      'file' => 'image.admin.inc',
     ),
     'image_anchor' => array(
       'render element' => 'element',
+      'file' => 'image.admin.inc',
     ),
     'image_resize_summary' => array(
       'variables' => array('data' => NULL),
+      'file' => 'image.admin.inc',
     ),
     'image_scale_summary' => array(
       'variables' => array('data' => NULL),
+      'file' => 'image.admin.inc',
     ),
     'image_crop_summary' => array(
       'variables' => array('data' => NULL),
+      'file' => 'image.admin.inc',
     ),
     'image_rotate_summary' => array(
       'variables' => array('data' => NULL),
+      'file' => 'image.admin.inc',
     ),
 
     // Theme functions in image.field.inc.
     'image_widget' => array(
       'render element' => 'element',
+      'file' => 'image.field.inc',
     ),
     'image_formatter' => array(
       'variables' => array('item' => NULL, 'path' => NULL, 'image_style' => NULL),
+      'file' => 'image.field.inc',
     ),
   );
 }
@@ -369,169 +369,6 @@ function image_style_options($include_empty = TRUE) {
   return $options;
 }
 
-/**
- * Returns a set of image effects.
- *
- * These image effects are exposed by modules implementing
- * hook_image_effect_info().
- *
- * @return
- *   An array of image effects to be used when transforming images.
- * @see hook_image_effect_info()
- * @see image_effect_definition_load()
- */
-function image_effect_definitions() {
-  $language_interface = language(Language::TYPE_INTERFACE);
-
-  // hook_image_effect_info() includes translated strings, so each language is
-  // cached separately.
-  $langcode = $language_interface->id;
-
-  $effects = &drupal_static(__FUNCTION__);
-
-  if (!isset($effects)) {
-    if ($cache = cache()->get("image_effects:$langcode")) {
-      $effects = $cache->data;
-    }
-    else {
-      $effects = array();
-      include_once __DIR__ . '/image.effects.inc';
-      foreach (module_implements('image_effect_info') as $module) {
-        foreach (module_invoke($module, 'image_effect_info') as $name => $effect) {
-          // Ensure the current toolkit supports the effect.
-          $effect['module'] = $module;
-          $effect['name'] = $name;
-          $effect['data'] = isset($effect['data']) ? $effect['data'] : array();
-          $effects[$name] = $effect;
-        }
-      }
-      uasort($effects, '_image_effect_definitions_sort');
-      drupal_alter('image_effect_info', $effects);
-      cache()->set("image_effects:$langcode", $effects);
-    }
-  }
-
-  return $effects;
-}
-
-/**
- * Loads the definition for an image effect.
- *
- * The effect definition is a set of core properties for an image effect, not
- * containing any user-settings. The definition defines various functions to
- * call when configuring or executing an image effect. This loader is mostly for
- * internal use within image.module. Use image_effect_load() or
- * entity_load() to get image effects that contain configuration.
- *
- * @param $effect
- *   The name of the effect definition to load.
- * @return
- *   An array containing the image effect definition with the following keys:
- *   - "effect": The unique name for the effect being performed. Usually prefixed
- *     with the name of the module providing the effect.
- *   - "module": The module providing the effect.
- *   - "help": A description of the effect.
- *   - "function": The name of the function that will execute the effect.
- *   - "form": (optional) The name of a function to configure the effect.
- *   - "summary": (optional) The name of a theme function that will display a
- *     one-line summary of the effect. Does not include the "theme_" prefix.
- */
-function image_effect_definition_load($effect) {
-  $definitions = image_effect_definitions();
-  return isset($definitions[$effect]) ? $definitions[$effect] : NULL;
-}
-
-/**
- * Loads a single image effect.
- *
- * @param $ieid
- *   The image effect ID.
- * @param $style_name
- *   The image style name.
- *
- * @return
- *   An image effect array, consisting of the following keys:
- *   - "ieid": The unique image effect ID.
- *   - "weight": The weight of this image effect within the image style.
- *   - "name": The name of the effect definition that powers this image effect.
- *   - "data": An array of configuration options for this image effect.
- *   Besides these keys, the entirety of the image definition is merged into
- *   the image effect array. Returns NULL if the specified effect cannot be
- *   found.
- * @see image_effect_definition_load()
- */
-function image_effect_load($ieid, $style_name) {
-  if (($style = entity_load('image_style', $style_name)) && isset($style->effects[$ieid])) {
-    $effect = $style->effects[$ieid];
-    $definition = image_effect_definition_load($effect['name']);
-    $effect = array_merge($definition, $effect);
-    // @todo The effect's key name within the style is unknown. It *should* be
-    //   identical to the ieid, but that is in no way guaranteed. And of course,
-    //   the ieid key *within* the effect is senseless duplication in the first
-    //   place. This problem can be eliminated in many places, but especially
-    //   for loaded menu arguments like %image_effect, the actual router
-    //   callbacks don't have access to 'ieid' anymore (unless resorting to
-    //   dirty %index and %map tricks).
-    $effect['ieid'] = $ieid;
-    return $effect;
-  }
-  return NULL;
-}
-
-/**
- * Saves an image effect.
- *
- * @param ImageStyle $style
- *   The image style this effect belongs to.
- * @param array $effect
- *   An image effect array. Passed by reference.
- *
- * @return array
- *   The saved image effect array. The 'ieid' key will be set for the effect.
- */
-function image_effect_save($style, &$effect) {
-  // Remove all values that are not properties of an image effect.
-  // @todo Convert image effects into plugins.
-  $effect = array_intersect_key($effect, array_flip(array('ieid', 'module', 'name', 'data', 'weight')));
-
-  // Generate a unique image effect ID for a new effect.
-  if (empty($effect['ieid'])) {
-    $uuid = new Uuid();
-    $effect['ieid'] = $uuid->generate();
-  }
-  $style->effects[$effect['ieid']] = $effect;
-  $style->save();
-}
-
-/**
- * Deletes an image effect.
- *
- * @param ImageStyle $style
- *   The image style this effect belongs to.
- * @param $effect
- *   An image effect array.
- */
-function image_effect_delete($style, $effect) {
-  unset($style->effects[$effect['ieid']]);
-  $style->save();
-}
-
-/**
- * Applies an image effect to the image object.
- *
- * @param $image
- *   An image object returned by image_load().
- * @param $effect
- *   An image effect array.
- * @return
- *   TRUE on success. FALSE if unable to perform the image effect on the image.
- */
-function image_effect_apply($image, $effect) {
-  module_load_include('inc', 'image', 'image.effects');
-  $function = $effect['effect callback'];
-  return $function($image, $effect['data']);
-}
-
 /**
  * Returns HTML for an image using a specific image style.
  *
@@ -609,15 +446,6 @@ function image_filter_keyword($value, $current_pixels, $new_pixels) {
   return $value;
 }
 
-/**
- * Internal function for sorting image effect definitions through uasort().
- *
- * @see image_effect_definitions()
- */
-function _image_effect_definitions_sort($a, $b) {
-  return strcasecmp($a['name'], $b['name']);
-}
-
 /**
  * Implements hook_entity_presave().
  *
diff --git a/core/modules/image/image.routing.yml b/core/modules/image/image.routing.yml
index 46a79f1f3d23..3625dbe0c077 100644
--- a/core/modules/image/image.routing.yml
+++ b/core/modules/image/image.routing.yml
@@ -19,3 +19,16 @@ image_style_private:
   requirements:
     _access: 'TRUE'
 
+image_effect_add_form:
+  pattern: '/admin/config/media/image-styles/manage/{image_style}/add/{image_effect}'
+  defaults:
+    _form: '\Drupal\image\Form\ImageEffectAddForm'
+  requirements:
+    _permission: 'administer image styles'
+
+image_effect_edit_form:
+  pattern: '/admin/config/media/image-styles/manage/{image_style}/effects/{image_effect}'
+  defaults:
+    _form: '\Drupal\image\Form\ImageEffectEditForm'
+  requirements:
+    _permission: 'administer image styles'
diff --git a/core/modules/image/image.services.yml b/core/modules/image/image.services.yml
index 2661593f7ad9..2af4f1c95d7b 100644
--- a/core/modules/image/image.services.yml
+++ b/core/modules/image/image.services.yml
@@ -7,3 +7,6 @@ services:
     class: Drupal\image\PathProcessor\PathProcessorImageStyles
     tags:
       - { name: path_processor_inbound, priority: 300 }
+  plugin.manager.image.effect:
+    class: Drupal\image\ImageEffectManager
+    arguments: ['@container.namespaces', '@cache.cache', '@language_manager', '@module_handler']
diff --git a/core/modules/image/lib/Drupal/image/Annotation/ImageEffect.php b/core/modules/image/lib/Drupal/image/Annotation/ImageEffect.php
new file mode 100644
index 000000000000..3a6f7636040c
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/Annotation/ImageEffect.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Annotation\ImageEffect.
+ */
+
+namespace Drupal\image\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines an image effect annotation object.
+ *
+ * @see hook_image_effect_info_alter()
+ *
+ * @Annotation
+ */
+class ImageEffect extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the image effect.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $label;
+
+  /**
+   * A brief description of the image effect.
+   *
+   * This will be shown when adding or configuring this image effect.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation (optional)
+   */
+  public $description = '';
+
+}
diff --git a/core/modules/image/lib/Drupal/image/ConfigurableImageEffectInterface.php b/core/modules/image/lib/Drupal/image/ConfigurableImageEffectInterface.php
new file mode 100644
index 000000000000..292aa4c47d87
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/ConfigurableImageEffectInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\ConfigurableImageEffectInterface.
+ */
+
+namespace Drupal\image;
+
+/**
+ * Defines the interface for configurable image effects.
+ */
+interface ConfigurableImageEffectInterface extends ImageEffectInterface {
+
+  /**
+   * Builds the part of the image effect form specific to this image effect.
+   *
+   * This method is only responsible for the form elements specific to this
+   * image effect. All other aspects of the form are handled by calling code.
+   *
+   * @return array
+   *   A render array.
+   */
+  public function getForm();
+
+}
diff --git a/core/modules/image/lib/Drupal/image/Form/ImageEffectAddForm.php b/core/modules/image/lib/Drupal/image/Form/ImageEffectAddForm.php
new file mode 100644
index 000000000000..1d644e6c166a
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/Form/ImageEffectAddForm.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Form\ImageEffectAddForm.
+ */
+
+namespace Drupal\image\Form;
+
+use Drupal\Core\Controller\ControllerInterface;
+use Drupal\image\ImageEffectManager;
+use Drupal\image\ImageStyleInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides an add form for image effects.
+ */
+class ImageEffectAddForm extends ImageEffectFormBase implements ControllerInterface {
+
+  /**
+   * The image effect manager.
+   *
+   * @var \Drupal\image\ImageEffectManager
+   */
+  protected $effectManager;
+
+  /**
+   * Constructs a new ImageEffectAddForm.
+   *
+   * @param \Drupal\image\ImageEffectManager $effect_manager
+   *   The image effect manager.
+   */
+  public function __construct(ImageEffectManager $effect_manager) {
+    $this->effectManager = $effect_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.image.effect')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state, Request $request = NULL, ImageStyleInterface $image_style = NULL, $image_effect = NULL) {
+    $form = parent::buildForm($form, $form_state, $request, $image_style, $image_effect);
+
+    drupal_set_title(t('Add %label effect', array('%label' => $this->imageEffect->label())), PASS_THROUGH);
+    $form['actions']['submit']['#value'] = t('Add effect');
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareImageEffect($image_effect) {
+    $image_effect = $this->effectManager->createInstance($image_effect);
+    // Set the initial weight so this effect comes last.
+    $image_effect->setWeight(count($this->imageStyle->getEffects()));
+    return $image_effect;
+  }
+
+}
diff --git a/core/modules/image/lib/Drupal/image/Form/ImageEffectDeleteForm.php b/core/modules/image/lib/Drupal/image/Form/ImageEffectDeleteForm.php
index 50bf2bb9c98b..5a75985dcd64 100644
--- a/core/modules/image/lib/Drupal/image/Form/ImageEffectDeleteForm.php
+++ b/core/modules/image/lib/Drupal/image/Form/ImageEffectDeleteForm.php
@@ -8,7 +8,7 @@
 namespace Drupal\image\Form;
 
 use Drupal\Core\Form\ConfirmFormBase;
-use Drupal\image\Plugin\Core\Entity\ImageStyle;
+use Drupal\image\ImageStyleInterface;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
@@ -19,14 +19,14 @@ class ImageEffectDeleteForm extends ConfirmFormBase {
   /**
    * The image style containing the image effect to be deleted.
    *
-   * @var \Drupal\image\Plugin\Core\Entity\ImageStyle
+   * @var \Drupal\image\ImageStyleInterface
    */
   protected $imageStyle;
 
   /**
    * The image effect to be deleted.
    *
-   * @var array;
+   * @var \Drupal\image\ImageEffectInterface
    */
   protected $imageEffect;
 
@@ -34,7 +34,7 @@ class ImageEffectDeleteForm extends ConfirmFormBase {
    * {@inheritdoc}
    */
   public function getQuestion() {
-    return t('Are you sure you want to delete the @effect effect from the %style style?', array('%style' => $this->imageStyle->label(), '@effect' => $this->imageEffect['label']));
+    return t('Are you sure you want to delete the @effect effect from the %style style?', array('%style' => $this->imageStyle->label(), '@effect' => $this->imageEffect->label()));
   }
 
   /**
@@ -61,9 +61,9 @@ public function getFormID() {
   /**
    * {@inheritdoc}
    */
-  public function buildForm(array $form, array &$form_state, $image_style = NULL, $image_effect = NULL, Request $request = NULL) {
+  public function buildForm(array $form, array &$form_state, ImageStyleInterface $image_style = NULL, $image_effect = NULL, Request $request = NULL) {
     $this->imageStyle = $image_style;
-    $this->imageEffect = image_effect_load($image_effect, $this->imageStyle->id());
+    $this->imageEffect = $this->imageStyle->getEffect($image_effect);
 
     return parent::buildForm($form, $form_state, $request);
   }
@@ -72,8 +72,8 @@ public function buildForm(array $form, array &$form_state, $image_style = NULL,
    * {@inheritdoc}
    */
   public function submitForm(array &$form, array &$form_state) {
-    image_effect_delete($this->imageStyle, $this->imageEffect);
-    drupal_set_message(t('The image effect %name has been deleted.', array('%name' => $this->imageEffect['label'])));
+    $this->imageStyle->deleteImageEffect($this->imageEffect);
+    drupal_set_message(t('The image effect %name has been deleted.', array('%name' => $this->imageEffect->label())));
     $form_state['redirect'] = 'admin/config/media/image-styles/manage/' . $this->imageStyle->id();
   }
 
diff --git a/core/modules/image/lib/Drupal/image/Form/ImageEffectEditForm.php b/core/modules/image/lib/Drupal/image/Form/ImageEffectEditForm.php
new file mode 100644
index 000000000000..57fda1638f57
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/Form/ImageEffectEditForm.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Form\ImageEffectEditForm.
+ */
+
+namespace Drupal\image\Form;
+
+use Drupal\image\ImageStyleInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides an edit form for image effects.
+ */
+class ImageEffectEditForm extends ImageEffectFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state, Request $request = NULL, ImageStyleInterface $image_style = NULL, $image_effect = NULL) {
+    $form = parent::buildForm($form, $form_state, $request, $image_style, $image_effect);
+
+    drupal_set_title(t('Edit %label effect', array('%label' => $this->imageEffect->label())), PASS_THROUGH);
+    $form['actions']['submit']['#value'] = t('Update effect');
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareImageEffect($image_effect) {
+    return $this->imageStyle->getEffect($image_effect);
+  }
+
+}
diff --git a/core/modules/image/lib/Drupal/image/Form/ImageEffectFormBase.php b/core/modules/image/lib/Drupal/image/Form/ImageEffectFormBase.php
new file mode 100644
index 000000000000..705dad3bb920
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/Form/ImageEffectFormBase.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Form\ImageEffectFormBase.
+ */
+
+namespace Drupal\image\Form;
+
+use Drupal\Core\Form\FormInterface;
+use Drupal\image\ConfigurableImageEffectInterface;
+use Drupal\image\ImageStyleInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Provides a base form for image effects.
+ */
+abstract class ImageEffectFormBase implements FormInterface {
+
+  /**
+   * The image style.
+   *
+   * @var \Drupal\image\ImageStyleInterface
+   */
+  protected $imageStyle;
+
+  /**
+   * The image effect.
+   *
+   * @var \Drupal\image\ImageEffectInterface
+   */
+  protected $imageEffect;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'image_effect_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   * @param \Drupal\image\ImageStyleInterface $image_style
+   *   The image style.
+   * @param string $image_effect
+   *   The image effect ID.
+   *
+   * @return array
+   *   The form structure.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+   */
+  public function buildForm(array $form, array &$form_state, Request $request = NULL, ImageStyleInterface $image_style = NULL, $image_effect = NULL) {
+    $this->imageStyle = $image_style;
+    $this->imageEffect = $this->prepareImageEffect($image_effect);
+
+    if (!($this->imageEffect instanceof ConfigurableImageEffectInterface)) {
+      throw new NotFoundHttpException();
+    }
+
+    $form['#attached']['css'][drupal_get_path('module', 'image') . '/css/image.admin.css'] = array();
+    $form['uuid'] = array(
+      '#type' => 'value',
+      '#value' => $this->imageEffect->getUuid(),
+    );
+    $form['id'] = array(
+      '#type' => 'value',
+      '#value' => $this->imageEffect->getPluginId(),
+    );
+
+    $form['data'] = $this->imageEffect->getForm();
+    $form['data']['#tree'] = TRUE;
+
+    // Check the URL for a weight, then the image effect, otherwise use default.
+    $form['weight'] = array(
+      '#type' => 'hidden',
+      '#value' => $request->query->has('weight') ? (int) $request->query->get('weight') : $this->imageEffect->getWeight(),
+    );
+
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#button_type' => 'primary',
+    );
+    $form['actions']['cancel'] = array(
+      '#type' => 'link',
+      '#title' => t('Cancel'),
+      '#href' => 'admin/config/media/image-styles/manage/' . $this->imageStyle->id(),
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    form_state_values_clean($form_state);
+    $this->imageStyle->saveImageEffect($form_state['values']);
+
+    drupal_set_message(t('The image effect was successfully applied.'));
+    $form_state['redirect'] = 'admin/config/media/image-styles/manage/' . $this->imageStyle->id();
+  }
+
+  /**
+   * Converts an image effect ID into an object.
+   *
+   * @param string $image_effect
+   *   The image effect ID.
+   *
+   * @return \Drupal\image\ImageEffectInterface
+   *   The image effect object.
+   */
+  abstract protected function prepareImageEffect($image_effect);
+
+}
diff --git a/core/modules/image/lib/Drupal/image/ImageEffectBag.php b/core/modules/image/lib/Drupal/image/ImageEffectBag.php
new file mode 100644
index 000000000000..0bdee3b6d75b
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/ImageEffectBag.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\ImageEffectBag.
+ */
+
+namespace Drupal\image;
+
+use Drupal\Component\Plugin\PluginBag;
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Component\Utility\MapArray;
+use Drupal\Component\Uuid\Uuid;
+
+/**
+ * A collection of image effects.
+ */
+class ImageEffectBag extends PluginBag {
+
+  /**
+   * The manager used to instantiate the plugins.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $manager;
+
+  /**
+   * The initial configuration for each image effect in the bag.
+   *
+   * @var array
+   */
+  protected $configurations = array();
+
+  /**
+   * Constructs a new ImageEffectBag.
+   *
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $manager
+   *   The manager to be used for instantiating plugins.
+   * @param array $configurations
+   *   (optional) An associative array containing the initial configuration for
+   *   each tour in the bag, keyed by plugin instance ID.
+   */
+  public function __construct(PluginManagerInterface $manager, array $configurations = array()) {
+    $this->manager = $manager;
+    $this->configurations = $configurations;
+
+    if (!empty($configurations)) {
+      $this->instanceIDs = MapArray::copyValuesToKeys(array_keys($configurations));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function initializePlugin($instance_id) {
+    if (!isset($this->pluginInstances[$instance_id])) {
+      $configuration = $this->configurations[$instance_id] + array('data' => array());
+      $this->pluginInstances[$instance_id] = $this->manager->createInstance($configuration['id'], $configuration);
+    }
+  }
+
+  /**
+   * Returns the current configuration of all image effects in this bag.
+   *
+   * @return array
+   *   An associative array keyed by image effect UUID, whose values are image
+   *   effect configurations.
+   */
+  public function export() {
+    $instances = array();
+    $this->rewind();
+    foreach ($this as $instance_id => $instance) {
+      $instances[$instance_id] = $instance->export();
+    }
+    return $instances;
+  }
+
+  /**
+   * Removes an instance ID.
+   *
+   * @param string $instance_id
+   *   An image effect instance IDs.
+   */
+  public function removeInstanceID($instance_id) {
+    unset($this->instanceIDs[$instance_id], $this->configurations[$instance_id]);
+    $this->remove($instance_id);
+  }
+
+  /**
+   * Updates the configuration for an image effect instance.
+   *
+   * If there is no plugin instance yet, a new will be instantiated. Otherwise,
+   * the existing instance is updated with the new configuration.
+   *
+   * @param array $configuration
+   *   The image effect configuration to set.
+   *
+   * @return string
+   */
+  public function setConfig(array $configuration) {
+    // Derive the instance ID from the configuration.
+    if (empty($configuration['uuid'])) {
+      $uuid_generator = new Uuid();
+      $configuration['uuid'] = $uuid_generator->generate();
+    }
+    $instance_id = $configuration['uuid'];
+    $this->configurations[$instance_id] = $configuration;
+    $this->get($instance_id)->setPluginConfiguration($configuration);
+    $this->addInstanceID($instance_id);
+    return $instance_id;
+  }
+
+  /**
+   * Sorts all image effect instances in this bag.
+   *
+   * @return self
+   */
+  public function sort() {
+    uasort($this->configurations, 'drupal_sort_weight');
+    $this->instanceIDs = MapArray::copyValuesToKeys(array_keys($this->configurations));
+    return $this;
+  }
+
+}
diff --git a/core/modules/image/lib/Drupal/image/ImageEffectBase.php b/core/modules/image/lib/Drupal/image/ImageEffectBase.php
new file mode 100644
index 000000000000..a4ddeb5bb155
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/ImageEffectBase.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Annotation\ImageEffectBase.
+ */
+
+namespace Drupal\image;
+
+use Drupal\Component\Plugin\PluginBase;
+
+/**
+ * Provides a base class for image effects.
+ */
+abstract class ImageEffectBase extends PluginBase implements ImageEffectInterface {
+
+  /**
+   * The image effect ID.
+   *
+   * @var string
+   */
+  protected $uuid;
+
+  /**
+   * The weight of the image effect.
+   *
+   * @var int|string
+   */
+  protected $weight = '';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->setPluginConfiguration($configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transformDimensions(array &$dimensions) {
+    $dimensions['width'] = $dimensions['height'] = NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSummary() {
+    return array(
+      '#markup' => '',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function label() {
+    return $this->pluginDefinition['label'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUuid() {
+    return $this->uuid;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setWeight($weight) {
+    $this->weight = $weight;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWeight() {
+    return $this->weight;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function export() {
+    return array(
+      'uuid' => $this->getUuid(),
+      'id' => $this->getPluginId(),
+      'weight' => $this->getWeight(),
+      'data' => $this->configuration,
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPluginConfiguration(array $configuration) {
+    $configuration += array(
+      'data' => array(),
+      'uuid' => '',
+      'weight' => '',
+    );
+    $this->configuration = $configuration['data'];
+    $this->uuid = $configuration['uuid'];
+    $this->weight = $configuration['weight'];
+    return $this;
+  }
+
+}
diff --git a/core/modules/image/lib/Drupal/image/ImageEffectInterface.php b/core/modules/image/lib/Drupal/image/ImageEffectInterface.php
new file mode 100644
index 000000000000..05d5efd89bfb
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/ImageEffectInterface.php
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\ImageEffectInterface.
+ */
+
+namespace Drupal\image;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * Defines the interface for image effects.
+ */
+interface ImageEffectInterface extends PluginInspectionInterface {
+
+  /**
+   * Applies an image effect to the image object.
+   *
+   * @param \stdClass $image
+   *   An image object returned by image_load().
+   *
+   * @return bool
+   *   TRUE on success. FALSE if unable to perform the image effect on the image.
+   */
+  public function applyEffect($image);
+
+  /**
+   * Determines the dimensions of the styled image.
+   *
+   * @param array $dimensions
+   *   Dimensions to be modified - an array with components width and height, in
+   *   pixels.
+   */
+  public function transformDimensions(array &$dimensions);
+
+  /**
+   * Returns a render array summarizing the configuration of the image effect.
+   *
+   * @return array
+   *   A render array.
+   */
+  public function getSummary();
+
+  /**
+   * Returns the image effect label.
+   *
+   * @return string
+   *   The image effect label.
+   */
+  public function label();
+
+  /**
+   * Returns the unique ID representing the image effect.
+   *
+   * @return string
+   *   The image effect ID.
+   */
+  public function getUuid();
+
+  /**
+   * Returns the weight of the image effect.
+   *
+   * @return int|string
+   *   Either the integer weight of the image effect, or an empty string.
+   */
+  public function getWeight();
+
+  /**
+   * Sets the weight for this image effect.
+   *
+   * @param int $weight
+   *   The weight for this image effect.
+   *
+   * @return self
+   *   This image effect.
+   */
+  public function setWeight($weight);
+
+  /**
+   * Exports the complete configuration of this image effect instance.
+   *
+   * @return array
+   */
+  public function export();
+
+  /**
+   * Sets the configuration for this image effect.
+   *
+   * @param array $configuration
+   *   An associative array containing:
+   *   - uuid: (optional) The image effect ID.
+   *   - weight: (optional) The weight of the image effect.
+   *   - data: (optional) An array of configuration specific to this image
+   *     effect type.
+   *
+   * @return self
+   *   This image effect.
+   */
+  public function setPluginConfiguration(array $configuration);
+
+}
diff --git a/core/modules/image/lib/Drupal/image/ImageEffectManager.php b/core/modules/image/lib/Drupal/image/ImageEffectManager.php
new file mode 100644
index 000000000000..95fc4c152027
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/ImageEffectManager.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\ImageEffectManager.
+ */
+
+namespace Drupal\image;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+/**
+ * Manages image effect plugins.
+ */
+class ImageEffectManager extends DefaultPluginManager {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler) {
+    $annotation_namespaces = array('Drupal\image\Annotation' => $namespaces['Drupal\image']);
+    parent::__construct('ImageEffect', $namespaces, $annotation_namespaces, 'Drupal\image\Annotation\ImageEffect');
+
+    $this->alterInfo($module_handler, 'image_effect_info');
+    $this->setCacheBackend($cache_backend, $language_manager, 'image_effect');
+  }
+
+}
diff --git a/core/modules/image/lib/Drupal/image/ImageStyleInterface.php b/core/modules/image/lib/Drupal/image/ImageStyleInterface.php
index 2532cc74084c..3783fc490106 100644
--- a/core/modules/image/lib/Drupal/image/ImageStyleInterface.php
+++ b/core/modules/image/lib/Drupal/image/ImageStyleInterface.php
@@ -8,6 +8,7 @@
 namespace Drupal\image;
 
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\image\ImageEffectInterface;
 
 /**
  * Provides an interface defining an image style entity.
@@ -51,6 +52,9 @@ public function buildUrl($path, $clean_urls = NULL);
    * @param string $path
    *   (optional) The original image path or URI. If it's supplied, only this
    *   image derivative will be flushed.
+   *
+   * @return self
+   *   This image style.
    */
   public function flush($path = NULL);
 
@@ -85,4 +89,45 @@ public function createDerivative($original_uri, $derivative_uri);
    */
   public function transformDimensions(array &$dimensions);
 
+  /**
+   * Returns a specific image effect.
+   *
+   * @param string $effect
+   *   The image effect ID.
+   *
+   * @return \Drupal\image\ImageEffectInterface
+   *   The image effect object.
+   */
+  public function getEffect($effect);
+
+  /**
+   * Returns the image effects for this style.
+   *
+   * @return \Drupal\image\ImageEffectBag|\Drupal\image\ImageEffectInterface[]
+   *   The image effect plugin bag.
+   */
+  public function getEffects();
+
+  /**
+   * Saves an image effect for this style.
+   *
+   * @param array $configuration
+   *   An array of image effect configuration.
+   *
+   * @return string
+   *   The image effect ID.
+   */
+  public function saveImageEffect(array $configuration);
+
+  /**
+   * Deletes an image effect from this style.
+   *
+   * @param \Drupal\image\ImageEffectInterface $effect
+   *   The image effect object.
+   *
+   * @return self
+   *   This image style.
+   */
+  public function deleteImageEffect(ImageEffectInterface $effect);
+
 }
diff --git a/core/modules/image/lib/Drupal/image/ImageStyleStorageController.php b/core/modules/image/lib/Drupal/image/ImageStyleStorageController.php
deleted file mode 100644
index 3f35f5b4e423..000000000000
--- a/core/modules/image/lib/Drupal/image/ImageStyleStorageController.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\image\ImageStyleStorageController.
- */
-
-namespace Drupal\image;
-
-use Drupal\Core\Config\Entity\ConfigStorageController;
-use Drupal\Core\Config\Config;
-
-/**
- * Defines a controller class for image styles.
- */
-class ImageStyleStorageController extends ConfigStorageController {
-
-  /**
-   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::attachLoad().
-   */
-  protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
-    foreach ($queried_entities as $style) {
-      if (!empty($style->effects)) {
-        foreach ($style->effects as $ieid => $effect) {
-          $definition = image_effect_definition_load($effect['name']);
-          $effect = array_merge($definition, $effect);
-          $style->effects[$ieid] = $effect;
-        }
-        // Sort effects by weight.
-        uasort($style->effects, 'drupal_sort_weight');
-      }
-    }
-    parent::attachLoad($queried_entities, $revision_id);
-  }
-
-}
diff --git a/core/modules/image/lib/Drupal/image/Plugin/Core/Entity/ImageStyle.php b/core/modules/image/lib/Drupal/image/Plugin/Core/Entity/ImageStyle.php
index 7fc752b19783..ed17deee21a9 100644
--- a/core/modules/image/lib/Drupal/image/Plugin/Core/Entity/ImageStyle.php
+++ b/core/modules/image/lib/Drupal/image/Plugin/Core/Entity/ImageStyle.php
@@ -11,6 +11,8 @@
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
 use Drupal\Core\Entity\EntityStorageControllerInterface;
+use Drupal\image\ImageEffectBag;
+use Drupal\image\ImageEffectInterface;
 use Drupal\image\ImageStyleInterface;
 use Drupal\Component\Utility\Crypt;
 use Drupal\Component\Utility\Url;
@@ -27,7 +29,7 @@
  *     "form" = {
  *       "delete" = "Drupal\image\Form\ImageStyleDeleteForm"
  *     },
- *     "storage" = "Drupal\image\ImageStyleStorageController"
+ *     "storage" = "Drupal\Core\Config\Entity\ConfigStorageController"
  *   },
  *   uri_callback = "image_style_entity_uri",
  *   config_prefix = "image.style",
@@ -73,7 +75,14 @@ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface {
    *
    * @var array
    */
-  public $effects;
+  protected $effects = array();
+
+  /**
+   * Holds the collection of image effects that are used by this image style.
+   *
+   * @var \Drupal\image\ImageEffectBag
+   */
+  protected $effectsBag;
 
   /**
    * Overrides Drupal\Core\Entity\Entity::id().
@@ -230,7 +239,7 @@ public function flush($path = NULL) {
       if (file_exists($derivative_uri)) {
         file_unmanaged_delete($derivative_uri);
       }
-      return;
+      return $this;
     }
 
     // Delete the style directory in each registered wrapper.
@@ -240,17 +249,19 @@ public function flush($path = NULL) {
     }
 
     // Let other modules update as necessary on flush.
-    \Drupal::moduleHandler()->invokeAll('image_style_flush', array($this));
+    $module_handler = \Drupal::moduleHandler();
+    $module_handler->invokeAll('image_style_flush', array($this));
 
     // Clear field caches so that formatters may be added for this style.
     field_info_cache_clear();
     drupal_theme_rebuild();
 
     // Clear page caches when flushing.
-    if (\Drupal::moduleHandler()->moduleExists('block')) {
-      cache('block')->deleteAll();
+    if ($module_handler->moduleExists('block')) {
+      \Drupal::cache('block')->deleteAll();
     }
-    cache('page')->deleteAll();
+    \Drupal::cache('page')->deleteAll();
+    return $this;
   }
 
   /**
@@ -270,10 +281,8 @@ public function createDerivative($original_uri, $derivative_uri) {
       return FALSE;
     }
 
-    if (!empty($this->effects)) {
-      foreach ($this->effects as $effect) {
-        image_effect_apply($image, $effect);
-      }
+    foreach ($this->getEffects() as $effect) {
+      $effect->applyEffect($image);
     }
 
     if (!image_save($image, $derivative_uri)) {
@@ -290,21 +299,8 @@ public function createDerivative($original_uri, $derivative_uri) {
    * {@inheritdoc}
    */
   public function transformDimensions(array &$dimensions) {
-    module_load_include('inc', 'image', 'image.effects');
-
-    if (!empty($this->effects)) {
-      foreach ($this->effects as $effect) {
-        if (isset($effect['dimensions passthrough'])) {
-          continue;
-        }
-
-        if (isset($effect['dimensions callback'])) {
-          $effect['dimensions callback']($dimensions, $effect['data']);
-        }
-        else {
-          $dimensions['width'] = $dimensions['height'] = NULL;
-        }
-      }
+    foreach ($this->getEffects() as $effect) {
+      $effect->transformDimensions($dimensions);
     }
   }
 
@@ -326,4 +322,48 @@ public function getPathToken($uri) {
     return substr(Crypt::hmacBase64($this->id() . ':' . $uri, drupal_get_private_key() . drupal_get_hash_salt()), 0, 8);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteImageEffect(ImageEffectInterface $effect) {
+    $this->getEffects()->removeInstanceID($effect->getUuid());
+    $this->save();
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEffect($effect) {
+    return $this->getEffects()->get($effect);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEffects() {
+    if (!$this->effectsBag) {
+      $this->effectsBag = new ImageEffectBag(\Drupal::service('plugin.manager.image.effect'), $this->effects);
+    }
+    return $this->effectsBag;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function saveImageEffect(array $configuration) {
+    $effect_id = $this->getEffects()->setConfig($configuration);
+    $this->save();
+    return $effect_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getExportProperties() {
+    $properties = parent::getExportProperties();
+    $properties['effects'] = $this->getEffects()->sort()->export();
+    return $properties;
+  }
+
 }
diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/CropImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/CropImageEffect.php
new file mode 100644
index 000000000000..11f228ed3025
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/CropImageEffect.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Plugin\ImageEffect\CropImageEffect.
+ */
+
+namespace Drupal\image\Plugin\ImageEffect;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\image\Annotation\ImageEffect;
+
+/**
+ * Crops an image resource.
+ *
+ * @ImageEffect(
+ *   id = "image_crop",
+ *   label = @Translation("Crop"),
+ *   description = @Translation("Resizing will make images an exact set of dimensions. This may cause images to be stretched or shrunk disproportionately.")
+ * )
+ */
+class CropImageEffect extends ResizeImageEffect {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyEffect($image) {
+    // Set sane default values.
+    $this->configuration += array(
+      'anchor' => 'center-center',
+    );
+
+    list($x, $y) = explode('-', $this->configuration['anchor']);
+    $x = image_filter_keyword($x, $image->info['width'], $this->configuration['width']);
+    $y = image_filter_keyword($y, $image->info['height'], $this->configuration['height']);
+    if (!image_crop($image, $x, $y, $this->configuration['width'], $this->configuration['height'])) {
+      watchdog('image', 'Image crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit->getPluginId(), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+      return FALSE;
+    }
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSummary() {
+    return array(
+      '#theme' => 'image_crop_summary',
+      '#data' => $this->configuration,
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getForm() {
+    $this->configuration += array(
+      'width' => '',
+      'height' => '',
+      'anchor' => 'center-center',
+    );
+    $form = parent::getForm();
+    $form['anchor'] = array(
+      '#type' => 'radios',
+      '#title' => t('Anchor'),
+      '#options' => array(
+        'left-top' => t('Top') . ' ' . t('Left'),
+        'center-top' => t('Top') . ' ' . t('Center'),
+        'right-top' => t('Top') . ' ' . t('Right'),
+        'left-center' => t('Center') . ' ' . t('Left'),
+        'center-center' => t('Center'),
+        'right-center' => t('Center') . ' ' . t('Right'),
+        'left-bottom' => t('Bottom') . ' ' . t('Left'),
+        'center-bottom' => t('Bottom') . ' ' . t('Center'),
+        'right-bottom' => t('Bottom') . ' ' . t('Right'),
+      ),
+      '#theme' => 'image_anchor',
+      '#default_value' => $this->configuration['anchor'],
+      '#description' => t('The part of the image that will be retained during the crop.'),
+    );
+    return $form;
+  }
+
+}
diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php
new file mode 100644
index 000000000000..816ed3e2b0e0
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Plugin\ImageEffect\DesaturateImageEffect.
+ */
+
+namespace Drupal\image\Plugin\ImageEffect;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\image\Annotation\ImageEffect;
+use Drupal\image\ImageEffectBase;
+
+/**
+ * Desaturates (grayscale) an image resource.
+ *
+ * @ImageEffect(
+ *   id = "image_desaturate",
+ *   label = @Translation("Desaturate"),
+ *   description = @Translation("Desaturate converts an image to grayscale.")
+ * )
+ */
+class DesaturateImageEffect extends ImageEffectBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transformDimensions(array &$dimensions) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyEffect($image) {
+    if (!image_desaturate($image)) {
+      watchdog('image', 'Image desaturate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit->getPluginId(), '%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/Plugin/ImageEffect/ResizeImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ResizeImageEffect.php
new file mode 100644
index 000000000000..484e6a0422ee
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ResizeImageEffect.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Plugin\ImageEffect\ResizeImageEffect.
+ */
+
+namespace Drupal\image\Plugin\ImageEffect;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\image\Annotation\ImageEffect;
+use Drupal\image\ConfigurableImageEffectInterface;
+use Drupal\image\ImageEffectBase;
+
+/**
+ * Resizes an image resource.
+ *
+ * @ImageEffect(
+ *   id = "image_resize",
+ *   label = @Translation("Resize"),
+ *   description = @Translation("Resizing will make images an exact set of dimensions. This may cause images to be stretched or shrunk disproportionately.")
+ * )
+ */
+class ResizeImageEffect extends ImageEffectBase implements ConfigurableImageEffectInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyEffect($image) {
+    if (!image_resize($image, $this->configuration['width'], $this->configuration['height'])) {
+      watchdog('image', 'Image resize failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit->getPluginId(), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+      return FALSE;
+    }
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transformDimensions(array &$dimensions) {
+    // The new image will have the exact dimensions defined for the effect.
+    $dimensions['width'] = $this->configuration['width'];
+    $dimensions['height'] = $this->configuration['height'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSummary() {
+    return array(
+      '#theme' => 'image_resize_summary',
+      '#data' => $this->configuration,
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getForm() {
+    $form['width'] = array(
+      '#type' => 'number',
+      '#title' => t('Width'),
+      '#default_value' => isset($this->configuration['width']) ? $this->configuration['width'] : '',
+      '#field_suffix' => ' ' . t('pixels'),
+      '#required' => TRUE,
+      '#min' => 1,
+    );
+    $form['height'] = array(
+      '#type' => 'number',
+      '#title' => t('Height'),
+      '#default_value' => isset($this->configuration['height']) ? $this->configuration['height'] : '',
+      '#field_suffix' => ' ' . t('pixels'),
+      '#required' => TRUE,
+      '#min' => 1,
+    );
+    return $form;
+  }
+
+}
diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php
new file mode 100644
index 000000000000..dee537e6b7d2
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Plugin\ImageEffect\RotateImageEffect.
+ */
+
+namespace Drupal\image\Plugin\ImageEffect;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\image\Annotation\ImageEffect;
+use Drupal\image\ConfigurableImageEffectInterface;
+use Drupal\image\ImageEffectBase;
+
+/**
+ * Rotates an image resource.
+ *
+ * @ImageEffect(
+ *   id = "image_rotate",
+ *   label = @Translation("Rotate"),
+ *   description = @Translation("Rotating an image may cause the dimensions of an image to increase to fit the diagonal.")
+ * )
+ */
+class RotateImageEffect extends ImageEffectBase implements ConfigurableImageEffectInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyEffect($image) {
+    // Set sane default values.
+    $this->configuration += array(
+      'degrees' => 0,
+      'bgcolor' => NULL,
+      'random' => FALSE,
+    );
+
+    // Convert short #FFF syntax to full #FFFFFF syntax.
+    if (strlen($this->configuration['bgcolor']) == 4) {
+      $c = $this->configuration['bgcolor'];
+      $this->configuration['bgcolor'] = $c[0] . $c[1] . $c[1] . $c[2] . $c[2] . $c[3] . $c[3];
+    }
+
+    // Convert #FFFFFF syntax to hexadecimal colors.
+    if ($this->configuration['bgcolor'] != '') {
+      $this->configuration['bgcolor'] = hexdec(str_replace('#', '0x', $this->configuration['bgcolor']));
+    }
+    else {
+      $this->configuration['bgcolor'] = NULL;
+    }
+
+    if (!empty($this->configuration['random'])) {
+      $degrees = abs((float) $this->configuration['degrees']);
+      $this->configuration['degrees'] = rand(-1 * $degrees, $degrees);
+    }
+
+    if (!image_rotate($image, $this->configuration['degrees'], $this->configuration['bgcolor'])) {
+      watchdog('image', 'Image rotate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit->getPluginId(), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+      return FALSE;
+    }
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transformDimensions(array &$dimensions) {
+    // If the rotate is not random and the angle is a multiple of 90 degrees,
+    // then the new dimensions can be determined.
+    if (!$this->configuration['random'] && ((int) ($this->configuration['degrees']) == $this->configuration['degrees']) && ($this->configuration['degrees'] % 90 == 0)) {
+      if ($this->configuration['degrees'] % 180 != 0) {
+        $temp = $dimensions['width'];
+        $dimensions['width'] = $dimensions['height'];
+        $dimensions['height'] = $temp;
+      }
+    }
+    else {
+      $dimensions['width'] = $dimensions['height'] = NULL;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSummary() {
+    return array(
+      '#theme' => 'image_rotate_summary',
+      '#data' => $this->configuration,
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getForm() {
+    $form['degrees'] = array(
+      '#type' => 'number',
+      '#default_value' => (isset($this->configuration['degrees'])) ? $this->configuration['degrees'] : 0,
+      '#title' => t('Rotation angle'),
+      '#description' => t('The number of degrees the image should be rotated. Positive numbers are clockwise, negative are counter-clockwise.'),
+      '#field_suffix' => '&deg;',
+      '#required' => TRUE,
+    );
+    $form['bgcolor'] = array(
+      '#type' => 'textfield',
+      '#default_value' => (isset($this->configuration['bgcolor'])) ? $this->configuration['bgcolor'] : '#FFFFFF',
+      '#title' => t('Background color'),
+      '#description' => t('The background color to use for exposed areas of the image. Use web-style hex colors (#FFFFFF for white, #000000 for black). Leave blank for transparency on image types that support it.'),
+      '#size' => 7,
+      '#maxlength' => 7,
+      '#element_validate' => array(array($this, 'validateColorEffect')),
+    );
+    $form['random'] = array(
+      '#type' => 'checkbox',
+      '#default_value' => (isset($this->configuration['random'])) ? $this->configuration['random'] : 0,
+      '#title' => t('Randomize'),
+      '#description' => t('Randomize the rotation angle for each image. The angle specified above is used as a maximum.'),
+    );
+    return $form;
+  }
+
+  /**
+   * Validates to ensure a hexadecimal color value.
+   */
+  public function validateColorEffect(array $element, array &$form_state) {
+    if ($element['#value'] != '') {
+      if (!preg_match('/^#[0-9A-F]{3}([0-9A-F]{3})?$/', $element['#value'])) {
+        form_error($element, t('!name must be a hexadecimal color value.', array('!name' => $element['#title'])));
+      }
+    }
+  }
+
+}
diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleAndCropImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleAndCropImageEffect.php
new file mode 100644
index 000000000000..dfa59550ef51
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleAndCropImageEffect.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Plugin\ImageEffect\ScaleAndCropImageEffect.
+ */
+
+namespace Drupal\image\Plugin\ImageEffect;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\image\Annotation\ImageEffect;
+
+/**
+ * Scales and crops an image resource.
+ *
+ * @ImageEffect(
+ *   id = "image_scale_and_crop",
+ *   label = @Translation("Scale and crop"),
+ *   description = @Translation("Scale and crop will maintain the aspect-ratio of the original image, then crop the larger dimension. This is most useful for creating perfectly square thumbnails without stretching the image.")
+ * )
+ */
+class ScaleAndCropImageEffect extends ResizeImageEffect {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyEffect($image) {
+    if (!image_scale_and_crop($image, $this->configuration['width'], $this->configuration['height'])) {
+      watchdog('image', 'Image scale and crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit->getPluginId(), '%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/Plugin/ImageEffect/ScaleImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleImageEffect.php
new file mode 100644
index 000000000000..47ada5a7a0eb
--- /dev/null
+++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleImageEffect.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Plugin\ImageEffect\ScaleImageEffect.
+ */
+
+namespace Drupal\image\Plugin\ImageEffect;
+
+use Drupal\Component\Image\Image;
+use Drupal\Core\Annotation\Translation;
+use Drupal\image\Annotation\ImageEffect;
+
+/**
+ * Scales an image resource.
+ *
+ * @ImageEffect(
+ *   id = "image_scale",
+ *   label = @Translation("Scale"),
+ *   description = @Translation("Scaling will maintain the aspect-ratio of the original image. If only a single dimension is specified, the other dimension will be calculated.")
+ * )
+ */
+class ScaleImageEffect extends ResizeImageEffect {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyEffect($image) {
+    // Set sane default values.
+    $this->configuration += array(
+      'width' => NULL,
+      'height' => NULL,
+      'upscale' => FALSE,
+    );
+
+    if (!image_scale($image, $this->configuration['width'], $this->configuration['height'], $this->configuration['upscale'])) {
+      watchdog('image', 'Image scale failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit->getPluginId(), '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+      return FALSE;
+    }
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transformDimensions(array &$dimensions) {
+    if ($dimensions['width'] && $dimensions['height']) {
+      Image::scaleDimensions($dimensions, $this->configuration['width'], $this->configuration['height'], $this->configuration['upscale']);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSummary() {
+    return array(
+      '#theme' => 'image_scale_summary',
+      '#data' => $this->configuration,
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getForm() {
+    $form = parent::getForm();
+    $form['#element_validate'] = array(array($this, 'validateScaleEffect'));
+    $form['width']['#required'] = FALSE;
+    $form['height']['#required'] = FALSE;
+    $form['upscale'] = array(
+      '#type' => 'checkbox',
+      '#default_value' => (isset($this->configuration['upscale'])) ? $this->configuration['upscale'] : 0,
+      '#title' => t('Allow Upscaling'),
+      '#description' => t('Let scale make images larger than their original size'),
+    );
+    return $form;
+  }
+
+  /**
+   * Validates to ensure that either a height or a width is specified.
+   */
+  public function validateScaleEffect(array $element, array &$form_state) {
+    if (empty($element['width']['#value']) && empty($element['height']['#value'])) {
+      form_error($element, t('Width and height can not both be blank.'));
+    }
+  }
+
+}
diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php
index 88eb3ad0c778..f34b246ed458 100644
--- a/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php
+++ b/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php
@@ -129,23 +129,23 @@ function testStyle() {
 
     // Confirm that all effects on the image style have settings on the effect
     // edit form that match what was saved.
-    $ieids = array();
-    foreach ($style->effects as $ieid => $effect) {
-      // Store the ieid for later use.
-      $ieids[$effect['name']] = $ieid;
-      $this->drupalGet($style_path . '/effects/' . $ieid);
-      foreach ($effect_edits[$effect['name']] as $field => $value) {
-        $this->assertFieldByName($field, $value, format_string('The %field field in the %effect effect has the correct value of %value.', array('%field' => $field, '%effect' => $effect['name'], '%value' => $value)));
+    $uuids = array();
+    foreach ($style->getEffects() as $uuid => $effect) {
+      // Store the uuid for later use.
+      $uuids[$effect->getPluginId()] = $uuid;
+      $this->drupalGet($style_path . '/effects/' . $uuid);
+      foreach ($effect_edits[$effect->getPluginId()] as $field => $value) {
+        $this->assertFieldByName($field, $value, format_string('The %field field in the %effect effect has the correct value of %value.', array('%field' => $field, '%effect' => $effect->getPluginId(), '%value' => $value)));
       }
     }
 
     // Assert that every effect was saved.
     foreach (array_keys($effect_edits) as $effect_name) {
-      $this->assertTrue(isset($ieids[$effect_name]), format_string(
-        'A %effect_name effect was saved with ID %ieid',
+      $this->assertTrue(isset($uuids[$effect_name]), format_string(
+        'A %effect_name effect was saved with ID %uuid',
         array(
           '%effect_name' => $effect_name,
-          '%ieid' => $ieids[$effect_name],
+          '%uuid' => $uuids[$effect_name],
         )));
     }
 
@@ -154,12 +154,13 @@ function testStyle() {
     // Confirm the order of effects is maintained according to the order we
     // added the fields.
     $effect_edits_order = array_keys($effect_edits);
-    $effects_order = array_values($style->effects);
     $order_correct = TRUE;
-    foreach ($effects_order as $index => $effect) {
-      if ($effect_edits_order[$index] != $effect['name']) {
+    $index = 0;
+    foreach ($style->getEffects()->sort() as $effect) {
+      if ($effect_edits_order[$index] != $effect->getPluginId()) {
         $order_correct = FALSE;
       }
+      $index++;
     }
     $this->assertTrue($order_correct, 'The order of the effects is correctly set by default.');
 
@@ -172,8 +173,8 @@ function testStyle() {
       'name' => $style_name,
       'label' => $style_label,
     );
-    foreach ($style->effects as $ieid => $effect) {
-      $edit['effects[' . $ieid . '][weight]'] = $weight;
+    foreach ($style->getEffects() as $uuid => $effect) {
+      $edit['effects[' . $uuid . '][weight]'] = $weight;
       $weight--;
     }
 
@@ -200,12 +201,13 @@ function testStyle() {
 
     // Confirm the new style order was saved.
     $effect_edits_order = array_reverse($effect_edits_order);
-    $effects_order = array_values($style->effects);
     $order_correct = TRUE;
-    foreach ($effects_order as $index => $effect) {
-      if ($effect_edits_order[$index] != $effect['name']) {
+    $index = 0;
+    foreach ($style->getEffects()->sort() as $effect) {
+      if ($effect_edits_order[$index] != $effect->getPluginId()) {
         $order_correct = FALSE;
       }
+      $index++;
     }
     $this->assertTrue($order_correct, 'The order of the effects is correctly set by default.');
 
@@ -216,19 +218,20 @@ function testStyle() {
     $this->assertEqual($this->getImageCount($style), 1, format_string('Image style %style image %file successfully generated.', array('%style' => $style->label(), '%file' => $image_path)));
 
     // Delete the 'image_crop' effect from the style.
-    $this->drupalPost($style_path . '/effects/' . $ieids['image_crop'] . '/delete', array(), t('Delete'));
+    $this->drupalPost($style_path . '/effects/' . $uuids['image_crop'] . '/delete', array(), t('Delete'));
     // Confirm that the form submission was successful.
     $this->assertResponse(200);
-    $this->assertRaw(t('The image effect %name has been deleted.', array('%name' => $style->effects[$ieids['image_crop']]['label'])));
+    $image_crop_effect = $style->getEffect($uuids['image_crop']);
+    $this->assertRaw(t('The image effect %name has been deleted.', array('%name' => $image_crop_effect->label())));
     // Confirm that there is no longer a link to the effect.
-    $this->assertNoLinkByHref($style_path . '/effects/' . $ieids['image_crop'] . '/delete');
+    $this->assertNoLinkByHref($style_path . '/effects/' . $uuids['image_crop'] . '/delete');
     // Refresh the image style information and verify that the effect was
     // actually deleted.
     $style = entity_load_unchanged('image_style', $style->id());
-    $this->assertFalse(isset($style->effects[$ieids['image_crop']]), format_string(
-      'Effect with ID %ieid no longer found on image style %style',
+    $this->assertFalse($style->getEffects()->has($uuids['image_crop']), format_string(
+      'Effect with ID %uuid no longer found on image style %style',
       array(
-        '%ieid' => $ieids['image_crop'],
+        '%uuid' => $uuids['image_crop'],
         '%style' => $style->label,
       )));
 
diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageDimensionsTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageDimensionsTest.php
index 8a08deecf913..0ff67388e362 100644
--- a/core/modules/image/lib/Drupal/image/Tests/ImageDimensionsTest.php
+++ b/core/modules/image/lib/Drupal/image/Tests/ImageDimensionsTest.php
@@ -59,7 +59,7 @@ function testImageDimensions() {
 
     // Scale an image that is wider than it is high.
     $effect = array(
-      'name' => 'image_scale',
+      'id' => 'image_scale',
       'data' => array(
         'width' => 120,
         'height' => 90,
@@ -68,7 +68,7 @@ function testImageDimensions() {
       'weight' => 0,
     );
 
-    image_effect_save($style, $effect);
+    $style->saveImageEffect($effect);
     $img_tag = theme_image_style($variables);
     $this->assertEqual($img_tag, '<img class="image-style-test" src="' . $url . '" width="120" height="60" alt="" />');
     $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
@@ -81,7 +81,7 @@ function testImageDimensions() {
 
     // Rotate 90 degrees anticlockwise.
     $effect = array(
-      'name' => 'image_rotate',
+      'id' => 'image_rotate',
       'data' => array(
         'degrees' => -90,
         'random' => FALSE,
@@ -89,7 +89,7 @@ function testImageDimensions() {
       'weight' => 1,
     );
 
-    image_effect_save($style, $effect);
+    $style->saveImageEffect($effect);
     $img_tag = theme_image_style($variables);
     $this->assertEqual($img_tag, '<img class="image-style-test" src="' . $url . '" width="60" height="120" alt="" />');
     $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
@@ -102,7 +102,7 @@ function testImageDimensions() {
 
     // Scale an image that is higher than it is wide (rotated by previous effect).
     $effect = array(
-      'name' => 'image_scale',
+      'id' => 'image_scale',
       'data' => array(
         'width' => 120,
         'height' => 90,
@@ -111,7 +111,7 @@ function testImageDimensions() {
       'weight' => 2,
     );
 
-    image_effect_save($style, $effect);
+    $style->saveImageEffect($effect);
     $img_tag = theme_image_style($variables);
     $this->assertEqual($img_tag, '<img class="image-style-test" src="' . $url . '" width="45" height="90" alt="" />');
     $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
@@ -124,7 +124,7 @@ function testImageDimensions() {
 
     // Test upscale disabled.
     $effect = array(
-      'name' => 'image_scale',
+      'id' => 'image_scale',
       'data' => array(
         'width' => 400,
         'height' => 200,
@@ -133,7 +133,7 @@ function testImageDimensions() {
       'weight' => 3,
     );
 
-    image_effect_save($style, $effect);
+    $style->saveImageEffect($effect);
     $img_tag = theme_image_style($variables);
     $this->assertEqual($img_tag, '<img class="image-style-test" src="' . $url . '" width="45" height="90" alt="" />');
     $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
@@ -146,12 +146,12 @@ function testImageDimensions() {
 
     // Add a desaturate effect.
     $effect = array(
-      'name' => 'image_desaturate',
+      'id' => 'image_desaturate',
       'data' => array(),
       'weight' => 4,
     );
 
-    image_effect_save($style, $effect);
+    $style->saveImageEffect($effect);
     $img_tag = theme_image_style($variables);
     $this->assertEqual($img_tag, '<img class="image-style-test" src="' . $url . '" width="45" height="90" alt="" />');
     $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
@@ -164,7 +164,7 @@ function testImageDimensions() {
 
     // Add a random rotate effect.
     $effect = array(
-      'name' => 'image_rotate',
+      'id' => 'image_rotate',
       'data' => array(
         'degrees' => 180,
         'random' => TRUE,
@@ -172,7 +172,7 @@ function testImageDimensions() {
       'weight' => 5,
     );
 
-    image_effect_save($style, $effect);
+    $style->saveImageEffect($effect);
     $img_tag = theme_image_style($variables);
     $this->assertEqual($img_tag, '<img class="image-style-test" src="' . $url . '" alt="" />');
     $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
@@ -183,7 +183,7 @@ function testImageDimensions() {
 
     // Add a crop effect.
     $effect = array(
-      'name' => 'image_crop',
+      'id' => 'image_crop',
       'data' => array(
         'width' => 30,
         'height' => 30,
@@ -192,7 +192,7 @@ function testImageDimensions() {
       'weight' => 6,
     );
 
-    image_effect_save($style, $effect);
+    $style->saveImageEffect($effect);
     $img_tag = theme_image_style($variables);
     $this->assertEqual($img_tag, '<img class="image-style-test" src="' . $url . '" width="30" height="30" alt="" />');
     $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
@@ -205,7 +205,7 @@ function testImageDimensions() {
 
     // Rotate to a non-multiple of 90 degrees.
     $effect = array(
-      'name' => 'image_rotate',
+      'id' => 'image_rotate',
       'data' => array(
         'degrees' => 57,
         'random' => FALSE,
@@ -213,7 +213,7 @@ function testImageDimensions() {
       'weight' => 7,
     );
 
-    image_effect_save($style, $effect);
+    $effect_id = $style->saveImageEffect($effect);
     $img_tag = theme_image_style($variables);
     $this->assertEqual($img_tag, '<img class="image-style-test" src="' . $url . '" alt="" />');
     $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
@@ -221,17 +221,18 @@ function testImageDimensions() {
     $this->assertResponse(200, 'Image was generated at the URL.');
     $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.');
 
-    image_effect_delete($style, $effect);
+    $effect_plugin = $style->getEffect($effect_id);
+    $style->deleteImageEffect($effect_plugin);
 
     // Ensure that an effect with no dimensions callback unsets the dimensions.
     // This ensures compatibility with 7.0 contrib modules.
     $effect = array(
-      'name' => 'image_module_test_null',
+      'id' => 'image_module_test_null',
       'data' => array(),
       'weight' => 8,
     );
 
-    image_effect_save($style, $effect);
+    $style->saveImageEffect($effect);
     $img_tag = theme_image_style($variables);
     $this->assertEqual($img_tag, '<img class="image-style-test" src="' . $url . '" alt="" />');
   }
diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php
index 7f24796a9e30..74f41eccf329 100644
--- a/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php
+++ b/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php
@@ -22,6 +22,13 @@ class ImageEffectsTest extends ToolkitTestBase {
    */
   public static $modules = array('image', 'image_test', 'image_module_test');
 
+  /**
+   * The image effect manager.
+   *
+   * @var \Drupal\image\ImageEffectManager
+   */
+  protected $manager;
+
   public static function getInfo() {
     return array(
       'name' => 'Image effects',
@@ -30,17 +37,19 @@ public static function getInfo() {
     );
   }
 
-  function setUp() {
+  public function setUp() {
     parent::setUp();
-
-    module_load_include('inc', 'image', 'image.effects');
+    $this->manager = $this->container->get('plugin.manager.image.effect');
   }
 
   /**
    * Test the image_resize_effect() function.
    */
   function testResizeEffect() {
-    $this->assertTrue(image_resize_effect($this->image, array('width' => 1, 'height' => 2)), 'Function returned the expected value.');
+    $this->assertImageEffect('image_resize', array(
+      'width' => 1,
+      'height' => 2,
+    ));
     $this->assertToolkitOperationsCalled(array('resize'));
 
     // Check the parameters.
@@ -54,7 +63,10 @@ function testResizeEffect() {
    */
   function testScaleEffect() {
     // @todo: need to test upscaling.
-    $this->assertTrue(image_scale_effect($this->image, array('width' => 10, 'height' => 10)), 'Function returned the expected value.');
+    $this->assertImageEffect('image_scale', array(
+      'width' => 10,
+      'height' => 10,
+    ));
     $this->assertToolkitOperationsCalled(array('resize'));
 
     // Check the parameters.
@@ -68,7 +80,11 @@ function testScaleEffect() {
    */
   function testCropEffect() {
     // @todo should test the keyword offsets.
-    $this->assertTrue(image_crop_effect($this->image, array('anchor' => 'top-1', 'width' => 3, 'height' => 4)), 'Function returned the expected value.');
+    $this->assertImageEffect('image_crop', array(
+      'anchor' => 'top-1',
+      'width' => 3,
+      'height' => 4,
+    ));
     $this->assertToolkitOperationsCalled(array('crop'));
 
     // Check the parameters.
@@ -83,7 +99,10 @@ function testCropEffect() {
    * Test the image_scale_and_crop_effect() function.
    */
   function testScaleAndCropEffect() {
-    $this->assertTrue(image_scale_and_crop_effect($this->image, array('width' => 5, 'height' => 10)), 'Function returned the expected value.');
+    $this->assertImageEffect('image_scale_and_crop', array(
+      'width' => 5,
+      'height' => 10,
+    ));
     $this->assertToolkitOperationsCalled(array('resize', 'crop'));
 
     // Check the parameters.
@@ -98,7 +117,7 @@ function testScaleAndCropEffect() {
    * Test the image_desaturate_effect() function.
    */
   function testDesaturateEffect() {
-    $this->assertTrue(image_desaturate_effect($this->image, array()), 'Function returned the expected value.');
+    $this->assertImageEffect('image_desaturate', array());
     $this->assertToolkitOperationsCalled(array('desaturate'));
 
     // Check the parameters.
@@ -111,7 +130,10 @@ function testDesaturateEffect() {
    */
   function testRotateEffect() {
     // @todo: need to test with 'random' => TRUE
-    $this->assertTrue(image_rotate_effect($this->image, array('degrees' => 90, 'bgcolor' => '#fff')), 'Function returned the expected value.');
+    $this->assertImageEffect('image_rotate', array(
+      'degrees' => 90,
+      'bgcolor' => '#fff',
+    ));
     $this->assertToolkitOperationsCalled(array('rotate'));
 
     // Check the parameters.
@@ -127,15 +149,32 @@ function testImageEffectsCaching() {
     $image_effect_definitions_called = &drupal_static('image_module_test_image_effect_info_alter');
 
     // First call should grab a fresh copy of the data.
-    $effects = image_effect_definitions();
+    $manager = $this->container->get('plugin.manager.image.effect');
+    $effects = $manager->getDefinitions();
     $this->assertTrue($image_effect_definitions_called === 1, 'image_effect_definitions() generated data.');
 
     // Second call should come from cache.
-    drupal_static_reset('image_effect_definitions');
     drupal_static_reset('image_module_test_image_effect_info_alter');
-    $cached_effects = image_effect_definitions();
-    $this->assertTrue(is_null($image_effect_definitions_called), 'image_effect_definitions() returned data from cache.');
+    $cached_effects = $manager->getDefinitions();
+    $this->assertTrue($image_effect_definitions_called === 0, 'image_effect_definitions() returned data from cache.');
 
     $this->assertTrue($effects == $cached_effects, 'Cached effects are the same as generated effects.');
   }
+
+  /**
+   * Asserts the effect processing of an image effect plugin.
+   *
+   * @param string $effect_name
+   *   The name of the image effect to test.
+   * @param array $data
+   *   The data to pass to the image effect.
+   *
+   * @return bool
+   *   TRUE if the assertion succeeded, FALSE otherwise.
+   */
+  protected function assertImageEffect($effect_name, array $data) {
+    $effect = $this->manager->createInstance($effect_name, array('data' => $data));
+    return $this->assertTrue($effect->applyEffect($this->image), 'Function returned the expected value.');
+  }
+
 }
diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageFieldTestBase.php b/core/modules/image/lib/Drupal/image/Tests/ImageFieldTestBase.php
index b1d171691919..7d0f0546938a 100644
--- a/core/modules/image/lib/Drupal/image/Tests/ImageFieldTestBase.php
+++ b/core/modules/image/lib/Drupal/image/Tests/ImageFieldTestBase.php
@@ -20,10 +20,6 @@
  * image.module:
  *   image_style_options()
  *   \Drupal\image\ImageStyleInterface::flush()
- *   image_effect_definition_load()
- *   image_effect_load()
- *   image_effect_save()
- *   image_effect_delete()
  *   image_filter_keyword()
  */
 
diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageStyleFlushTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageStyleFlushTest.php
index 101171b92b41..b5c3776a44a1 100644
--- a/core/modules/image/lib/Drupal/image/Tests/ImageStyleFlushTest.php
+++ b/core/modules/image/lib/Drupal/image/Tests/ImageStyleFlushTest.php
@@ -100,11 +100,11 @@ function testFlush() {
     // Remove the 'image_scale' effect and updates the style, which in turn
     // forces an image style flush.
     $style_path = 'admin/config/media/image-styles/manage/' . $style->id();
-    $ieids = array();
-    foreach ($style->effects as $ieid => $effect) {
-      $ieids[$effect['name']] = $ieid;
+    $uuids = array();
+    foreach ($style->getEffects() as $uuid => $effect) {
+      $uuids[$effect->getPluginId()] = $uuid;
     }
-    $this->drupalPost($style_path . '/effects/' . $ieids['image_scale'] . '/delete', array(), t('Delete'));
+    $this->drupalPost($style_path . '/effects/' . $uuids['image_scale'] . '/delete', array(), t('Delete'));
     $this->assertResponse(200);
     $this->drupalPost($style_path, array(), t('Update style'));
     $this->assertResponse(200);
diff --git a/core/modules/image/tests/image_module_test.module b/core/modules/image/tests/image_module_test.module
deleted file mode 100644
index 7cc6cdd97cb1..000000000000
--- a/core/modules/image/tests/image_module_test.module
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides Image module hook implementations for testing purposes.
- */
-
-function image_module_test_file_download($uri) {
-  $default_uri = Drupal::state()->get('image.test_file_download') ?: FALSE;
-  if ($default_uri == $uri) {
-    return array('X-Image-Owned-By' => 'image_module_test');
-  }
-}
-
-/**
- * Implements hook_image_effect_info().
- */
-function image_module_test_image_effect_info() {
-  $effects = array(
-    'image_module_test_null' => array(
-    'effect callback' => 'image_module_test_null_effect',
-    ),
-  );
-
-  return $effects;
-}
-
-/**
- * Image effect callback; Null.
- *
- * @param $image
- *   An image object returned by image_load().
- * @param $data
- *   An array with no attributes.
- *
- * @return
- *   TRUE
- */
-function image_module_test_null_effect(array &$image, array $data) {
-  return TRUE;
-}
-
-/**
- * Implements hook_image_effect_info_alter().
- *
- * Used to keep a count of cache misses in image_effect_definitions().
- */
-function image_module_test_image_effect_info_alter(&$effects) {
-  $image_effects_definition_called = &drupal_static(__FUNCTION__, 0);
-  $image_effects_definition_called++;
-}
diff --git a/core/modules/image/tests/image_module_test.info.yml b/core/modules/image/tests/modules/image_module_test/image_module_test.info.yml
similarity index 100%
rename from core/modules/image/tests/image_module_test.info.yml
rename to core/modules/image/tests/modules/image_module_test/image_module_test.info.yml
diff --git a/core/modules/image/tests/modules/image_module_test/image_module_test.module b/core/modules/image/tests/modules/image_module_test/image_module_test.module
new file mode 100644
index 000000000000..7d64d2c86fa0
--- /dev/null
+++ b/core/modules/image/tests/modules/image_module_test/image_module_test.module
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Provides Image module hook implementations for testing purposes.
+ */
+
+function image_module_test_file_download($uri) {
+  $default_uri = Drupal::state()->get('image.test_file_download') ?: FALSE;
+  if ($default_uri == $uri) {
+    return array('X-Image-Owned-By' => 'image_module_test');
+  }
+}
+
+/**
+ * Implements hook_image_effect_info_alter().
+ *
+ * Used to keep a count of cache misses in \Drupal\image\ImageEffectManager.
+ */
+function image_module_test_image_effect_info_alter(&$effects) {
+  $image_effects_definition_called = &drupal_static(__FUNCTION__, 0);
+  $image_effects_definition_called++;
+}
diff --git a/core/modules/image/tests/modules/image_module_test/lib/Drupal/image_module_test/Plugin/ImageEffect/NullTestImageEffect.php b/core/modules/image/tests/modules/image_module_test/lib/Drupal/image_module_test/Plugin/ImageEffect/NullTestImageEffect.php
new file mode 100644
index 000000000000..23b1931450ba
--- /dev/null
+++ b/core/modules/image/tests/modules/image_module_test/lib/Drupal/image_module_test/Plugin/ImageEffect/NullTestImageEffect.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_module_test\Plugin\ImageEffect\NullTestImageEffect.
+ */
+
+namespace Drupal\image_module_test\Plugin\ImageEffect;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\image\Annotation\ImageEffect;
+use Drupal\image\ImageEffectBase;
+
+/**
+ * Performs no operation on an image resource.
+ *
+ * @ImageEffect(
+ *   id = "image_module_test_null",
+ *   label = @Translation("Image module test")
+ * )
+ */
+class NullTestImageEffect extends ImageEffectBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyEffect($image) {
+    return TRUE;
+  }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Upgrade/ImageUpgradePathTest.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/ImageUpgradePathTest.php
index fc248133f384..cc5979ab468d 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Upgrade/ImageUpgradePathTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/ImageUpgradePathTest.php
@@ -38,7 +38,7 @@ public function testImageStyleUpgrade() {
       'name' => 'test-custom',
       'effects' => array(
         'image_rotate' => array(
-          'name' => 'image_rotate',
+          'id' => 'image_rotate',
           'data' => array(
             'degrees' => '90',
             'bgcolor' => '#FFFFFF',
@@ -47,7 +47,7 @@ public function testImageStyleUpgrade() {
           'weight' => '1',
         ),
         'image_desaturate' => array(
-          'name' => 'image_desaturate',
+          'id' => 'image_desaturate',
           'data' => array(),
           'weight' => '2',
         ),
@@ -57,7 +57,7 @@ public function testImageStyleUpgrade() {
       'name' => 'thumbnail',
       'effects' => array (
         'image_scale' => array(
-          'name' => 'image_scale',
+          'id' => 'image_scale',
           'data' => array (
             'width' => '177',
             'height' => '177',
@@ -73,11 +73,11 @@ public function testImageStyleUpgrade() {
       // during by the image style upgrade functions.
       foreach ($config->get('effects') as $uuid => $effect) {
         // Copy placeholder data.
-        $style['effects'][$uuid] = $style['effects'][$effect['name']];
-        // Set the missing ieid key as this is unknown because it is a UUID.
-        $style['effects'][$uuid]['ieid'] = $uuid;
+        $style['effects'][$uuid] = $style['effects'][$effect['id']];
+        // Set the missing uuid key as this is unknown because it is a UUID.
+        $style['effects'][$uuid]['uuid'] = $uuid;
         // Remove the placeholder data.
-        unset($style['effects'][$effect['name']]);
+        unset($style['effects'][$effect['id']]);
       }
       $this->assertEqual($this->sortByKey($style), $config->get(), format_string('@first is equal to @second.', array(
         '@first' => var_export($this->sortByKey($style), TRUE),
-- 
GitLab