diff --git a/core/modules/image/css/editors/image.css b/core/modules/image/css/editors/image.css
new file mode 100644
index 0000000..4427354
--- /dev/null
+++ b/core/modules/image/css/editors/image.css
@@ -0,0 +1,112 @@
+.quickedit-image-element {
+  min-width: 200px;
+  min-height: 200px;
+}
+
+.quickedit-image-dropzone {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(116, 183, 255, 0.8);
+  transition: background .2s;
+}
+
+.quickedit-image-dropzone.hover {
+  background: rgba(116, 183, 255, 0.9);
+}
+
+.quickedit-image-dropzone.error {
+  background: rgba(255, 52, 27, 0.81);
+}
+
+.quickedit-image-dropzone * {
+  pointer-events: none;
+}
+
+.quickedit-image-icon {
+  display: block;
+  width: 50px;
+  height: 50px;
+  margin: 0 0 10px 0;
+  background-repeat: no-repeat;
+  background-size: cover;
+  transition: margin .5s;
+}
+
+.quickedit-image-dropzone.upload .quickedit-image-icon {
+  background-image: url('../../icons/upload.svg');
+}
+
+.quickedit-image-dropzone.error .quickedit-image-icon {
+  background-image: url('../../icons/error.svg');
+}
+
+.quickedit-image-dropzone.loading .quickedit-image-icon {
+  margin: -10px 0 20px 0;
+}
+
+.quickedit-image-dropzone.loading .quickedit-image-icon::after {
+  display: block;
+  content: "";
+  margin-left: -10px;
+  margin-top: -5px;
+  animation-duration: 2s;
+  animation-name: animation;
+  animation-iteration-count: infinite;
+  animation-timing-function: linear;
+  width: 60px;
+  height: 60px;
+  border-style: solid;
+  border-radius: 35px;
+  border-width: 5px;
+  border-color: white transparent transparent transparent;
+}
+
+@keyframes animation {
+  0% {transform: rotate(0deg);}
+  50% {transform: rotate(180deg);}
+  100% {transform: rotate(360deg);}
+}
+
+.quickedit-image-text {
+  display: block;
+  text-align: center;
+  color: white;
+  font-family: "Droid sans", "Lucida Grande", sans-serif;
+  pointer-events: none;
+  font-size: 16px;
+  -webkit-user-select: none;
+}
+
+.quickedit-image-field-info {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  background: rgba(0, 0, 0, 0.05);
+  border-top: 1px solid #c5c5c5;
+  padding: 5px;
+}
+
+.quickedit-image-field-info label, .quickedit-image-field-info input {
+  margin-right: 5px;
+}
+
+.quickedit-image-field-info input:last-child {
+  margin-right: 0;
+}
+
+.quickedit-image-errors .messages__wrapper {
+  margin: 0;
+  padding: 0;
+}
+
+.quickedit-image-errors .messages--error {
+  box-shadow: none;
+}
+
diff --git a/core/modules/image/icons/error.svg b/core/modules/image/icons/error.svg
new file mode 100644
index 0000000..1c53789
--- /dev/null
+++ b/core/modules/image/icons/error.svg
@@ -0,0 +1,4 @@
+<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none"/>
+    <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
+</svg>
\ No newline at end of file
diff --git a/core/modules/image/icons/upload.svg b/core/modules/image/icons/upload.svg
new file mode 100644
index 0000000..168bc43
--- /dev/null
+++ b/core/modules/image/icons/upload.svg
@@ -0,0 +1,4 @@
+<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none"/>
+    <path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/>
+</svg>
diff --git a/core/modules/image/image.libraries.yml b/core/modules/image/image.libraries.yml
index e9061a4..46afd5a 100644
--- a/core/modules/image/image.libraries.yml
+++ b/core/modules/image/image.libraries.yml
@@ -3,3 +3,13 @@ admin:
   css:
     theme:
       css/image.admin.css: {}
+
+quickedit.inPlaceEditor.image:
+  version: VERSION
+  js:
+    js/editors/image.js: {}
+  css:
+    component:
+      css/editors/image.css: {}
+  dependencies:
+    - quickedit/quickedit
diff --git a/core/modules/image/image.routing.yml b/core/modules/image/image.routing.yml
index ffeed86..9d400ab 100644
--- a/core/modules/image/image.routing.yml
+++ b/core/modules/image/image.routing.yml
@@ -71,3 +71,29 @@ image.effect_edit_form:
 
 route_callbacks:
   - '\Drupal\image\Routing\ImageStyleRoutes::routes'
+
+image.upload:
+  path: '/quickedit/image/upload/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
+  defaults:
+    _controller: '\Drupal\image\Controller\QuickeditImageController::upload'
+  options:
+    parameters:
+      entity:
+        type: entity:{entity_type}
+  requirements:
+    _permission: 'access in-place editing'
+    _access_quickedit_entity_field: 'TRUE'
+    _method: 'POST'
+
+image.info:
+  path: '/quickedit/image/info/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
+  defaults:
+    _controller: '\Drupal\image\Controller\QuickeditImageController::getInfo'
+  options:
+    parameters:
+      entity:
+        type: entity:{entity_type}
+  requirements:
+    _permission: 'access in-place editing'
+    _access_quickedit_entity_field: 'TRUE'
+    _method: 'GET'
diff --git a/core/modules/image/js/editors/image.js b/core/modules/image/js/editors/image.js
new file mode 100644
index 0000000..f9985d7
--- /dev/null
+++ b/core/modules/image/js/editors/image.js
@@ -0,0 +1,351 @@
+/**
+ * @file
+ * Drag+drop based in-place editor for images.
+ */
+
+(function ($, _, Drupal) {
+
+  'use strict';
+
+  Drupal.quickedit.editors.image = Drupal.quickedit.EditorView.extend(/** @lends Drupal.quickedit.editors.image# */{
+
+    /**
+     * @constructs
+     *
+     * @augments Drupal.quickedit.EditorView
+     *
+     * @param {object} options
+     *   Options for the plain text editor.
+     */
+    initialize: function (options) {
+      Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
+      // Set our original value to our current HTML (for reverting).
+      this.model.set('originalValue', $.trim(this.$el.html()));
+      // $.val() callback function for copying input from our custom form to
+      // the Quickedit Field Form.
+      this.model.set('currentValue', function (index, value) {
+        var matches = $(this).attr('name').match(/(alt|title)]$/);
+        if (matches) {
+          var name = matches[1];
+          var $input = $('.quickedit-image-field-info input[name="' + name + '"]');
+          if ($input.length) {
+            return $input.val();
+          }
+        }
+      });
+    },
+
+    /**
+     * Template to display errors inline with the toolbar.
+     */
+    template_errors: _.template(
+      '<div class="quickedit-image-errors">' +
+      '  <%= errors %>' +
+      '</div>'
+    ),
+
+    /**
+     * Template to display the dropzone area.
+     */
+    template_dropzone: _.template(
+      '<div class="quickedit-image-dropzone <%- state %>">' +
+      '  <i class="quickedit-image-icon"></i>' +
+      '  <span class="quickedit-image-text"><%- text %></span>' +
+      '</div>'
+    ),
+
+    /**
+     * Template to display the toolbar.
+     */
+    template_toolbar: _.template(
+      '<form class="quickedit-image-field-info">' +
+      '<% if (alt_field) { %>' +
+      '  <label for="alt" class="<% if (alt_field_required) { %>form-required<%} %>">Alt</label>' +
+      '  <input type="text" placeholder="<%- alt %>" value="<%- alt %>" name="alt" <% if (alt_field_required) { %>required<%} %>/>' +
+      '<% } %>' +
+      '<% if (title_field) { %>' +
+      '  <label for="title" class="<% if (alt_field_required) { %>form-required<%} %>">Title</label>' +
+      '  <input type="text" placeholder="<%- title %>" value="<%- title %>" name="title" <% if (title_field_required) { %>required<%} %>/>' +
+      '<% } %>' +
+      '</form>'
+    ),
+
+    /**
+     * @inheritdoc
+     *
+     * @param {Drupal.quickedit.FieldModel} fieldModel
+     *   The field model that holds the state.
+     * @param {string} state
+     *   The state to change to.
+     * @param {object} options
+     *   State options, if needed by the state change.
+     */
+    stateChange: function (fieldModel, state, options) {
+      var from = fieldModel.previous('state');
+      switch (state) {
+        case 'inactive':
+          break;
+
+        case 'candidate':
+          if (from !== 'inactive') {
+            this.$el.find('.quickedit-image-dropzone').remove();
+            this.$el.removeClass('quickedit-image-element');
+          }
+          if (from === 'invalid') {
+            this.removeValidationErrors();
+          }
+          break;
+
+        case 'highlighted':
+          break;
+
+        case 'activating':
+          // Defer updating the field model until the current state change has
+          // propagated, to not trigger a nested state change event.
+          _.defer(function () {
+            fieldModel.set('state', 'active');
+          });
+          break;
+
+        case 'active':
+          // Once active, render the dropzone area and our custom toolbar form.
+          var self = this;
+
+          // Indicate that this element is being edited by Quickedit Image.
+          this.$el.addClass('quickedit-image-element');
+
+          var $dropzone = this.renderDropzone('upload', Drupal.t('Drag file here or click to upload'));
+
+          // Generic event callback to stop event behavior and bubbling.
+          var stopEvent = function (e) {
+            e.preventDefault();
+            e.stopPropagation();
+          };
+
+          // Prevent the browser's default behavior when dragging files onto
+          // the document (usually opens them in the same tab).
+          $dropzone.on('dragover', stopEvent);
+          $dropzone.on('dragenter', function (e) {
+            stopEvent(e);
+            $(this).addClass('hover');
+          });
+          $dropzone.on('dragleave', function (e) {
+            stopEvent(e);
+            $(this).removeClass('hover');
+          });
+
+          $dropzone.on('drop', function (e){
+            stopEvent(e);
+
+            // Only respond when a file is dropped (could be another element).
+            if(e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files.length){
+              $(this).removeClass('hover');
+              self.uploadImage(e.originalEvent.dataTransfer.files[0]);
+            }
+          });
+
+          $dropzone.on('click', function (e) {
+            stopEvent(e);
+            var $input = $('<input type="file">').trigger('click');
+            $input.change(function () {
+              if (this.files.length) {
+                self.uploadImage(this.files[0]);
+              }
+            });
+          });
+
+          this.renderToolbar(fieldModel);
+          break;
+
+        case 'changed':
+          break;
+
+        case 'saving':
+          if (from === 'invalid') {
+            this.removeValidationErrors();
+          }
+
+          // Before we submit, validate the alt/title text fields.
+          this.save(options);
+          break;
+
+        case 'saved':
+          break;
+
+        case 'invalid':
+          this.showValidationErrors();
+          break;
+      }
+    },
+
+    /**
+     * Validates/uploads a given file.
+     *
+     * @param {File} file
+     *   The file to upload.
+     */
+    uploadImage: function (file) {
+      // Indicate loading by adding a special class to our icon.
+      this.renderDropzone('upload loading', Drupal.t('Uploading <i>@file</i>...', {'@file': file.name}));
+
+      // Build a valid URL for our endpoint.
+      var fieldID = this.fieldModel.get('fieldID');
+      var url = Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/image/upload/!entity_type/!id/!field_name/!langcode/!view_mode'));
+
+      // Construct form data that our endpoint can consume.
+      var data = new FormData();
+      data.append('files[image]', file);
+
+      // Construct a POST request to our endpoint.
+      var self = this;
+      this.ajax('POST', url, data, function (response) {
+        var $el = $(self.fieldModel.get('el'));
+        // Indicate that the field has changed - this enables the
+        // "Save" button.
+        self.fieldModel.set('state', 'changed');
+        self.fieldModel.get('entity').set('inTempStore', true);
+        self.removeValidationErrors();
+
+        // Replace our innerHTML with the new image. If we replaced
+        // our entire element with data.html, we would have to
+        // implement complicated logic like what's in
+        // Drupal.quickedit.AppView.renderUpdatedField.
+        var content = $(response.html).closest('[data-quickedit-field-id]').html();
+        $el.html(content);
+      });
+    },
+
+    /**
+     * Utility function to make an AJAX request to the server.
+     *
+     * @param {string} type
+     *   The type of request (i.e. GET, POST, PUT, DELETE, etc.)
+     * @param {string} url
+     *   The URL for the request.
+     * @param {*} data
+     *   The data to send to the server.
+     * @param {function} callback
+     *   A callback function used when a request is successful, without errors.
+     */
+    ajax: function (type, url, data, callback) {
+      $.ajax({
+        url: url,
+        context: this,
+        type: type,
+        data: data,
+        dataType: 'json',
+        cache: false,
+        contentType: false,
+        processData: false,
+        success: function(response) {
+          if (response.main_error) {
+            this.renderDropzone('error', response.main_error);
+            if (response.errors.length) {
+              this.model.set('validationErrors', response.errors);
+            }
+            this.showValidationErrors();
+          }
+          else {
+            callback(response);
+          }
+        },
+        error: function() {
+          this.renderDropzone('error', Drupal.t('A server error has occurred.'));
+        }
+      });
+    },
+
+    /**
+     * Renders our toolbar form for editing metadata.
+     *
+     * @param {Drupal.quickedit.FieldModel} fieldModel
+     *   The current Field Model.
+     */
+    renderToolbar: function (fieldModel) {
+      var $toolbar = $('.quickedit-image-field-info');
+      if ($toolbar.length === 0) {
+        // Perform an AJAX request for extra image info (alt/title).
+        var fieldID = fieldModel.get('fieldID');
+        var url = Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/image/info/!entity_type/!id/!field_name/!langcode/!view_mode'));
+        var self = this;
+        self.ajax('GET', url, null, function (response) {
+          $toolbar = $(self.template_toolbar(response));
+          $('#' + fieldModel.toolbarView.getMainWysiwygToolgroupId()).append($toolbar);
+          $toolbar.on('keyup paste', function () {
+            fieldModel.set('state', 'changed');
+          });
+          // Re-position the toolbar, which could have changed size.
+          fieldModel.get('entity').toolbarView.position();
+        });
+      }
+    },
+
+    /**
+     * Renders our dropzone element.
+     *
+     * @param {string} state
+     *   The current state of our editor. Only used for visual styling.
+     * @param {string} text
+     *   The text to display in the dropzone area.
+     */
+    renderDropzone: function (state, text) {
+      var $dropzone = this.$el.find('.quickedit-image-dropzone');
+      // If the element already exists, modify its contents.
+      if ($dropzone.length) {
+        $dropzone.removeClass('upload error hover loading');
+        $dropzone.addClass('.quickedit-image-dropzone ' + state);
+        $dropzone.children('.quickedit-image-text').html(text);
+      }
+      else {
+        $dropzone = $(this.template_dropzone({
+          state: state,
+          text: text
+        }));
+        this.$el.append($dropzone);
+      }
+
+      return $dropzone;
+    },
+
+    /**
+     * @inheritdoc
+     */
+    revert: function () {
+      this.$el.html(this.model.get('originalValue'));
+    },
+
+    /**
+     * @inheritdoc
+     */
+    getQuickEditUISettings: function () {
+      return {padding: false, unifiedToolbar: true, fullWidthToolbar: true, popup: false};
+    },
+
+    /**
+     * @inheritdoc
+     */
+    showValidationErrors: function () {
+      var $errors = $(this.template_errors({
+        errors: this.model.get('validationErrors')
+      }));
+      $('#' + this.fieldModel.toolbarView.getMainWysiwygToolgroupId())
+        .append($errors);
+      this.getEditedElement()
+        .addClass('quickedit-validation-error');
+      // Re-position the toolbar, which could have changed size.
+      this.fieldModel.get('entity').toolbarView.position();
+    },
+
+    /**
+     * @inheritdoc
+     */
+    removeValidationErrors: function () {
+      $('#' + this.fieldModel.toolbarView.getMainWysiwygToolgroupId())
+        .find('.quickedit-image-errors').remove();
+      this.getEditedElement()
+        .removeClass('quickedit-validation-error');
+    }
+
+  });
+
+})(jQuery, _, Drupal);
diff --git a/core/modules/image/src/Controller/QuickeditImageController.php b/core/modules/image/src/Controller/QuickeditImageController.php
new file mode 100644
index 0000000..f564d11
--- /dev/null
+++ b/core/modules/image/src/Controller/QuickeditImageController.php
@@ -0,0 +1,208 @@
+<?php
+
+namespace Drupal\image\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Image\ImageFactory;
+use Drupal\Core\Render\Element\StatusMessages;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\image\Plugin\Field\FieldType\ImageItem;
+use Drupal\user\PrivateTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * Returns responses for our image routes.
+ */
+class QuickeditImageController extends ControllerBase {
+
+  /**
+   * Stores the Quickedit tempstore.
+   *
+   * @var \Drupal\user\PrivateTempStore
+   */
+  protected $tempStore;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The image factory.
+   *
+   * @var \Drupal\Core\Image\ImageFactory
+   */
+  protected $imageFactory;
+
+  /**
+   * Constructs a new QuickeditImageController.
+   *
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Drupal\Core\Image\ImageFactory $image_factory
+   *   The image factory.
+   * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
+   *   The tempstore factory.
+   */
+  public function __construct(RendererInterface $renderer, ImageFactory $image_factory, PrivateTempStoreFactory $temp_store_factory) {
+    $this->renderer = $renderer;
+    $this->imageFactory = $image_factory;
+    $this->tempStore = $temp_store_factory->get('quickedit');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('renderer'),
+      $container->get('image.factory'),
+      $container->get('user.private_tempstore')
+    );
+  }
+
+  /**
+   * Returns a JSON object representing the new file upload, or validation
+   * errors.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity of which an image field is being rendered.
+   * @param string $field_name
+   *   The name of the (image) field that is being rendered
+   * @param string $langcode
+   *   The language code of the field that is being rendered.
+   * @param string $view_mode_id
+   *   The view mode of the field that is being rendered.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
+   *   The Ajax response.
+   */
+  public function upload(EntityInterface $entity, $field_name, $langcode, $view_mode_id) {
+    $field = $this->getField($entity, $field_name, $langcode);
+    $field_validators = $field->getUploadValidators();
+    $field_settings = $field->getFieldDefinition()->getSettings();
+    $destination = $field->getUploadLocation();
+
+    // Add upload resolution validation.
+    if ($field_settings['max_resolution'] || $field_settings['min_resolution']) {
+      $field_validators['file_validate_image_resolution'] = [$field_settings['max_resolution'], $field_settings['min_resolution']];
+    }
+
+    // Create the destination directory if it does not already exist.
+    if (isset($destination) && !file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
+      return new JsonResponse(['main_error' => $this->t('The destination directory could not be created.'), 'errors' => '']);
+    }
+
+    // Attempt to save the image given the field's constraints.
+    $result = file_save_upload('image', $field_validators, $destination);
+    if (is_array($result) && $result[0]) {
+      /** @var \Drupal\file\Entity\File $file */
+      $file = $result[0];
+      $image = $this->imageFactory->get($file->getFileUri());
+
+      // Set the value in the Entity to the new file.
+      /** @var \Drupal\file\Plugin\Field\FieldType\FileFieldItemList $field_list */
+      $value = $entity->$field_name->getValue();
+      $value[0]['target_id'] = $file->id();
+      $value[0]['width'] = $image->getWidth();
+      $value[0]['height'] = $image->getHeight();
+      $entity->$field_name->setValue($value);
+
+      // Render the new image using the correct formatter settings.
+      $entity_view_mode_ids = array_keys($this->entityManager()->getViewModes($entity->getEntityTypeId()));
+      if (in_array($view_mode_id, $entity_view_mode_ids)) {
+        $output = $entity->$field_name->view($view_mode_id);
+      }
+      else {
+        // Each part of a custom (non-Entity Display) view mode ID is separated
+        // by a dash; the first part must be the module name.
+        $mode_id_parts = explode('-', $view_mode_id, 2);
+        $module = reset($mode_id_parts);
+        $args = [$entity, $field_name, $view_mode_id, $langcode];
+        $output = $this->moduleHandler()->invoke($module, 'quickedit_render_field', $args);
+      }
+
+      // Save the Entity to tempstore.
+      $this->tempStore->set($entity->uuid(), $entity);
+
+      $data = [
+        'fid' => $file->id(),
+        'html' => $this->renderer->renderRoot($output),
+      ];
+      return new JsonResponse($data);
+    }
+    else {
+      // Return a JSON object containing the errors from Drupal and our
+      // "main_error", which is displayed inside the dropzone area.
+      $messages = StatusMessages::renderMessages('error');
+      return new JsonResponse(['errors' => $this->renderer->render($messages), 'main_error' => $this->t('The requested image failed validation.')]);
+    }
+  }
+
+  /**
+   * Returns a JSON object representing an image field's metadata.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity of which an image field is being rendered.
+   * @param string $field_name
+   *   The name of the (image) field that is being rendered
+   * @param string $langcode
+   *   The language code of the field that is being rendered.
+   * @param string $view_mode_id
+   *   The view mode of the field that is being rendered.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
+   *   The Ajax response.
+   */
+  public function getInfo(EntityInterface $entity, $field_name, $langcode, $view_mode_id) {
+    $field = $this->getField($entity, $field_name, $langcode);
+    $settings = $field->getFieldDefinition()->getSettings();
+    $info = [
+      'alt' => $field->alt,
+      'title' => $field->title,
+      'alt_field' => $settings['alt_field'],
+      'title_field' => $settings['title_field'],
+      'alt_field_required' => $settings['alt_field_required'],
+      'title_field_required' => $settings['title_field_required'],
+    ];
+    return new JsonResponse($info);
+  }
+
+  /**
+   * Returns a JSON object representing the current state of the field.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity of which an image field is being rendered.
+   * @param string $field_name
+   *   The name of the (image) field that is being rendered
+   * @param string $langcode
+   *   The language code of the field that is being rendered.
+   * @return \Drupal\image\Plugin\Field\FieldType\ImageItem
+   *   The field for this request.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   *   Throws an exception if the request is invalid.
+   */
+  protected function getField(EntityInterface $entity, $field_name, $langcode) {
+    // Ensure that this is a valid Entity.
+    if (!($entity instanceof ContentEntityInterface)) {
+      throw new BadRequestHttpException('Requested Entity is not a Content Entity.');
+    }
+
+    // Get the first item (we only support single-cardinality image fields).
+    $field = $entity->getTranslation($langcode)->$field_name->get(0);
+
+    if (!($field instanceof ImageItem)) {
+      throw new BadRequestHttpException('Requested Field is not of type "image".');
+    }
+
+    return $field;
+  }
+
+}
diff --git a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php
index 4c3a27d..c1ee4df 100644
--- a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php
+++ b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php
@@ -22,6 +22,9 @@
  *   label = @Translation("Image"),
  *   field_types = {
  *     "image"
+ *   },
+ *   quickedit = {
+ *     "editor" = "image"
  *   }
  * )
  */
diff --git a/core/modules/image/src/Plugin/InPlaceEditor/Image.php b/core/modules/image/src/Plugin/InPlaceEditor/Image.php
new file mode 100644
index 0000000..b81b5bf
--- /dev/null
+++ b/core/modules/image/src/Plugin/InPlaceEditor/Image.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\image\Plugin\InPlaceEditor;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\quickedit\Plugin\InPlaceEditorBase;
+
+/**
+ * Defines the image text in-place editor.
+ *
+ * @InPlaceEditor(
+ *   id = "image"
+ * )
+ */
+class Image extends InPlaceEditorBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCompatible(FieldItemListInterface $items) {
+    $field_definition = $items->getFieldDefinition();
+
+    // This editor is only compatible with single-value image fields.
+    if ($field_definition->getFieldStorageDefinition()->getCardinality() == 1
+      && $field_definition->getType() == 'image') {
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAttachments() {
+    return [
+      'library' => [
+        'image/quickedit.inPlaceEditor.image',
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php
index ff91e68..3ec1a19 100644
--- a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php
+++ b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php
@@ -23,6 +23,9 @@
  *   label = @Translation("Responsive image"),
  *   field_types = {
  *     "image",
+ *   },
+ *   quickedit = {
+ *     "editor" = "image"
  *   }
  * )
  */
