diff --git a/Build/Resources/Public/Less/TYPO3/_element_cropper.less b/Build/Resources/Public/Less/TYPO3/_element_cropper.less
index 0404c0148477db01c776de6d8f3f2b533ecf42fa..853a93bc30edfc894ec45eaf1fdd5c30849bade2 100644
--- a/Build/Resources/Public/Less/TYPO3/_element_cropper.less
+++ b/Build/Resources/Public/Less/TYPO3/_element_cropper.less
@@ -442,4 +442,4 @@
 		max-width: none !important;
 		max-height: none !important;
 	}
-}
\ No newline at end of file
+}
diff --git a/typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php b/typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php
index 903affac09ad09691ecbf621e249d896d61fc3c8..3d2b9ff8cb3fd5c77d41dedf4f274c52101845b1 100644
--- a/typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php
+++ b/typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php
@@ -120,7 +120,9 @@ class ImageManipulationElement extends AbstractFormElement
         parent::__construct($nodeFactory, $data);
         // Would be great, if we could inject the view here, but since the constructor is in the interface, we can't
         $this->templateView = GeneralUtility::makeInstance(StandaloneView::class);
-        $this->templateView->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/ImageManipulation/ImageCropping.html'));
+        $this->templateView->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts/')]);
+        $this->templateView->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials/ImageManipulation/')]);
+        $this->templateView->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/ImageManipulation/ImageManipulationElement.html'));
         $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
     }
 
@@ -136,29 +138,30 @@ class ImageManipulationElement extends AbstractFormElement
         $parameterArray = $this->data['parameterArray'];
         $config = $this->populateConfiguration($parameterArray['fieldConf']['config']);
 
-        if ($config['readOnly']) {
-            $options = [];
-            $options['parameterArray'] = [
-                'fieldConf' => [
-                    'config' => $parameterArray['fieldConf']['config'],
-                ],
-                'itemFormElValue' => $parameterArray['itemFormElValue'],
-            ];
-            $options['renderType'] = 'none';
-
-            // Early return in case the field is set to read only
-            return $this->nodeFactory->create($options)->render();
-        }
-
         $file = $this->getFile($this->data['databaseRow'], $config['file_field']);
         if (!$file) {
             // Early return in case we do not find a file
             return $resultArray;
         }
 
-        $config = $this->processConfiguration($config, $parameterArray['itemFormElValue'] ?? '{}');
+        $config = $this->processConfiguration($config, $parameterArray['itemFormElValue'], $file);
+
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+        $fieldControlResult = $this->renderFieldControl();
+        $fieldControlHtml = $fieldControlResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
+
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
 
         $arguments = [
+            'fieldInformation' => $fieldInformationHtml,
+            'fieldControl' => $fieldControlHtml,
+            'fieldWizard' => $fieldWizardHtml,
             'isAllowedFileExtension' => in_array(strtolower($file->getExtension()), GeneralUtility::trimExplode(',', strtolower($config['allowedExtensions'])), true),
             'image' => $file,
             'formEngine' => [
@@ -182,7 +185,8 @@ class ImageManipulationElement extends AbstractFormElement
                 $arguments['formEngine']['validation'] = $this->getValidationDataAsJsonString(['required' => true]);
             }
         }
-        $resultArray['html'] = $this->templateView->renderSection('Element', $arguments);
+        $this->templateView->assignMultiple($arguments);
+        $resultArray['html'] = $this->templateView->render();
 
         return $resultArray;
     }
@@ -271,13 +275,18 @@ class ImageManipulationElement extends AbstractFormElement
     /**
      * @param array $config
      * @param string $elementValue
+     * @param File $file
      * @return array
      * @throws \TYPO3\CMS\Core\Imaging\ImageManipulation\InvalidConfigurationException
      */
-    protected function processConfiguration(array $config, string $elementValue)
+    protected function processConfiguration(array $config, string &$elementValue, File $file)
     {
         $cropVariantCollection = CropVariantCollection::create($elementValue, $config['cropVariants']);
+        if (empty($config['readOnly'])) {
+            $cropVariantCollection = $cropVariantCollection->applyRatioRestrictionToSelectedCropArea($file);
+        }
         $config['cropVariants'] = $cropVariantCollection->asArray();
+        $elementValue = (string)$cropVariantCollection;
         $config['allowedExtensions'] = implode(', ', GeneralUtility::trimExplode(',', $config['allowedExtensions'], true));
         return $config;
     }
diff --git a/typo3/sysext/backend/Classes/Form/Wizard/ImageManipulationWizard.php b/typo3/sysext/backend/Classes/Form/Wizard/ImageManipulationWizard.php
index a176c5e62d0dd8b1860c94c54a16b1ca8b75f2e7..112d956bd0d98b581c248a76ddd21c0205f665a4 100644
--- a/typo3/sysext/backend/Classes/Form/Wizard/ImageManipulationWizard.php
+++ b/typo3/sysext/backend/Classes/Form/Wizard/ImageManipulationWizard.php
@@ -40,7 +40,9 @@ class ImageManipulationWizard
     {
         if (!$templateView) {
             $templateView = GeneralUtility::makeInstance(StandaloneView::class);
-            $templateView->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/ImageManipulation/ImageCropping.html'));
+            $templateView->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts/')]);
+            $templateView->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials/ImageManipulation/')]);
+            $templateView->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/ImageManipulation/ImageManipulationWizard.html'));
         }
         $this->templateView = $templateView;
     }
@@ -68,7 +70,7 @@ class ImageManipulationWizard
                 'image' => $image,
                 'cropVariants' => $queryParams['cropVariants']
             ];
-            $content = $this->templateView->renderSection('Cropper', $viewData);
+            $content = $this->templateView->renderSection('Main', $viewData);
             $response->getBody()->write($content);
 
             return $response;
diff --git a/typo3/sysext/backend/Resources/Private/Layouts/ImageManipulation.html b/typo3/sysext/backend/Resources/Private/Layouts/ImageManipulation.html
new file mode 100644
index 0000000000000000000000000000000000000000..05351ecbc8ba66a0f88e352109805d440575fc33
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Private/Layouts/ImageManipulation.html
@@ -0,0 +1,16 @@
+<div class="t3js-formengine-field-item">
+  {fieldInformation -> f:format.raw()}
+  <div class="form-wizards-wrap">
+      <div class="form-wizards-element">
+          <f:render section="Main" />
+      </div>
+     <div class="form-wizards-items-aside">
+         <div class="btn-group">
+             {fieldControl -> f:format.raw()}
+         </div>
+     </div>
+      <div class="form-wizards-items-bottom">
+          {fieldWizard -> f:format.raw()}
+      </div>
+  </div>
+</div>
diff --git a/typo3/sysext/backend/Resources/Private/Partials/ImageManipulation/ModalTitle.html b/typo3/sysext/backend/Resources/Private/Partials/ImageManipulation/ModalTitle.html
new file mode 100644
index 0000000000000000000000000000000000000000..f251983a57402251a06270419727bd4ce2d0e428
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Private/Partials/ImageManipulation/ModalTitle.html
@@ -0,0 +1,9 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers">
+
+	<f:section name="Main">
+		<f:translate id="LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:imwizard.image-manipulation" />
+		: <f:if condition="{image.properties.title}"><f:then>{image.properties.title} &mdash; {image.name}</f:then><f:else>{image.name}</f:else></f:if>
+		({image.properties.width} × {image.properties.height})
+	</f:section>
+
+</html>
diff --git a/typo3/sysext/backend/Resources/Private/Templates/ImageManipulation/ImageManipulationElement.html b/typo3/sysext/backend/Resources/Private/Templates/ImageManipulation/ImageManipulationElement.html
new file mode 100644
index 0000000000000000000000000000000000000000..61d526f6a8f5cde4cb72f8f9d0b9d3759ac8daef
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Private/Templates/ImageManipulation/ImageManipulationElement.html
@@ -0,0 +1,61 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
+	xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers">
+	<f:layout name="ImageManipulation" />
+
+	<f:section name="Main">
+		<div class="media">
+			<f:if condition="{isAllowedFileExtension}">
+				<f:then>
+					<div class="media-left">
+						<f:for each="{config.cropVariants}" as="cropVariant">
+							<div class="t3js-image-manipulation-preview media-object" data-preview-width="150" data-preview-height="200" data-crop-variant-id="{cropVariant.id}">
+								<f:image image="{image}" crop="{formEngine.field.value}" cropVariant="{cropVariant.id}" maxWidth="150" maxHeight="200" class="thumbnail thumbnail-status" additionalAttributes="{data-crop-variant: '{cropVariant -> f:format.json()}', data-crop-variant-id: cropVariant.id}" />
+							</div>
+						</f:for>
+					</div>
+					<f:if condition="{config.readOnly}">
+						<f:else>
+							<div class="media-body">
+								<input type="hidden" id="{formEngine.field.id}" name="{formEngine.field.name}" value="{formEngine.field.value}" data-formengine-validation-rules="{formEngine.validation}" />
+								<button class="btn btn-default t3js-image-manipulation-trigger"
+												data-url="{wizardUri}"
+												data-preview-url="{previewUrl}"
+												data-severity="notice"
+												data-modal-title="{f:render(partial: 'ModalTitle', section:'Main', arguments: _all)}"
+												data-image-uid="{image.uid}"
+												data-crop-variants="{config.cropVariants -> f:format.json()}"
+												data-file-field="{config.file_field}"
+												data-field="{formEngine.field.id}">
+									<span class="t3-icon fa fa-crop"></span><f:translate id="LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:imwizard.open-editor" />
+								</button>
+								<f:if condition="{crop}" >
+									<div class="table-fit-block table-spacer-wrap">
+										<table class="table table-no-borders t3js-image-manipulation-info">
+											<tr>
+												<td><f:translate id="LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:imwizard.crop-width" /></td>
+												<td class="t3js-image-manipulation-info-crop-width">{crop.width}px</td>
+											</tr>
+											<tr>
+												<td><f:translate id="LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:imwizard.crop-height" /></td>
+												<td class="t3js-image-manipulation-info-crop-height">{crop.height}px</td>
+											</tr>
+										</table>
+									</div>
+								</f:if>
+							</div>
+						</f:else>
+					</f:if>
+				</f:then>
+				<f:else>
+					<div class="media-body">
+					<p><em>
+						<f:translate id="LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:imwizard.supported-types-message" />
+						<br/>
+						{config.allowedExtensions -> f:format.case(mode: 'upper')}
+					</em></p>
+					</div>
+				</f:else>
+			</f:if>
+		</div>
+	</f:section>
+</html>
diff --git a/typo3/sysext/backend/Resources/Private/Templates/ImageManipulation/ImageCropping.html b/typo3/sysext/backend/Resources/Private/Templates/ImageManipulation/ImageManipulationWizard.html
similarity index 66%
rename from typo3/sysext/backend/Resources/Private/Templates/ImageManipulation/ImageCropping.html
rename to typo3/sysext/backend/Resources/Private/Templates/ImageManipulation/ImageManipulationWizard.html
index 7a2a03dc7ea1a33d5f72a9032eff8a7afacb4e4e..fd9e97ac192456d88324a850f04c5216e08bc13b 100644
--- a/typo3/sysext/backend/Resources/Private/Templates/ImageManipulation/ImageCropping.html
+++ b/typo3/sysext/backend/Resources/Private/Templates/ImageManipulation/ImageManipulationWizard.html
@@ -1,74 +1,20 @@
 <html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
 	xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers">
 
-	<f:section name="Element">
-		<div class="media">
-			<f:if condition="{isAllowedFileExtension}">
-				<f:then>
-					<div class="media-left">
-						<f:for each="{config.cropVariants}" as="cropVariant">
-							<div class="t3js-image-manipulation-preview media-object" data-preview-width="150" data-preview-height="200" data-crop-variant-id="{cropVariant.id}">
-								<f:image image="{image}" crop="{formEngine.field.value}" cropVariant="{cropVariant.id}" maxWidth="150" maxHeight="200" class="thumbnail thumbnail-status" additionalAttributes="{data-crop-variant: '{cropVariant -> f:format.json()}', data-crop-variant-id: cropVariant.id}" />
-							</div>
-						</f:for>
-					</div>
-					<div class="media-body">
-						<input type="hidden" id="{formEngine.field.id}" name="{formEngine.field.name}" value="{formEngine.field.value}" data-formengine-validation-rules="{formEngine.validation}" />
-						<button class="btn btn-default t3js-image-manipulation-trigger"
-										data-url="{wizardUri}"
-										data-preview-url="{previewUrl}"
-										data-severity="notice"
-										data-modal-title="{f:render(section: 'ModalTitle', arguments: _all)}"
-										data-image-uid="{image.uid}"
-										data-crop-variants="{config.cropVariants -> f:format.json()}"
-										data-file-field="{config.file_field}"
-										data-field="{formEngine.field.id}">
-							<span class="t3-icon fa fa-crop"></span><f:translate id="LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:imwizard.open-editor" />
-						</button>
-						<f:if condition="{crop}" >
-							<div class="table-fit-block table-spacer-wrap">
-								<table class="table table-no-borders t3js-image-manipulation-info">
-									<tr>
-										<td><f:translate id="LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:imwizard.crop-width" /></td>
-										<td class="t3js-image-manipulation-info-crop-width">{crop.width}px</td>
-									</tr>
-									<tr>
-										<td><f:translate id="LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:imwizard.crop-height" /></td>
-										<td class="t3js-image-manipulation-info-crop-height">{crop.height}px</td>
-									</tr>
-								</table>
-							</div>
-						</f:if>
-					</div>
-				</f:then>
-				<f:else>
-					<div class="media-body">
-					<p><em>
-						<f:translate id="LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:imwizard.supported-types-message" />
-						<br/>
-						{config.allowedExtensions -> f:format.case(mode: 'upper')}
-					</em></p>
-					</div>
-				</f:else>
-			</f:if>
-		</div>
-	</f:section>
-	<f:section name="Cropper">
+	<f:section name="Main">
 		<f:if condition="{image.properties.width}">
 			<f:then>
 				<div class="modal-header">
 					<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span>
 					</button>
 					<h4 class="modal-title">
-						{f:render(section: 'ModalTitle', arguments: _all)}
+						{f:render(partial: 'ModalTitle', section:'Main', arguments: _all)}
 					</h4>
 				</div>
 				<div class="cropper modal-panel">
 					<div class="modal-panel-body">
-						<div class="cropper-image-container">
-							<img id="t3js-crop-image" class="cropper-image-container-image"
-									 src="{f:uri.image(image:image, maxWidth:'1000', maxHeight: '700')}"
-									 data-original-width="{image.properties.width}" data-original-height="{image.properties.height}"/>
+						<div id="t3js-crop-image-container" class="cropper-image-container">
+							<f:image image="{image}" id="t3js-crop-image" class="cropper-image-container-image" additionalAttributes="{data-original-width: image.properties.width, data-original-height: image.properties.height}" />
 						</div>
 					</div>
 					<div class="modal-panel-sidebar modal-panel-sidebar-right">
@@ -175,9 +121,4 @@
 			</f:else>
 		</f:if>
 	</f:section>
-	<f:section name="ModalTitle">
-		{f:translate(id: 'LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:imwizard.image-manipulation')}
-		: {f:if(condition:image.properties.title, then:image.properties.title, else:image.name)}
-		({image.properties.width} × {image.properties.height})
-	</f:section>
 </html>
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/ImageManipulation.ts b/typo3/sysext/backend/Resources/Private/TypeScript/ImageManipulation.ts
index 722a0b39caa37871e426a9fe2a3b2da1daf90d1d..956aaa65402d39eb29df5a79d54946467fff831c 100644
--- a/typo3/sysext/backend/Resources/Private/TypeScript/ImageManipulation.ts
+++ b/typo3/sysext/backend/Resources/Private/TypeScript/ImageManipulation.ts
@@ -150,6 +150,8 @@ class ImageManipulation {
   private aspectRatioTrigger: JQuery;
   private cropperCanvas: JQuery;
   private cropInfo: JQuery;
+  private cropImageContainerSelector: string = '#t3js-crop-image-container';
+  private cropImageSelector: string = '#t3js-crop-image';
   private coverAreaSelector: string = '.t3js-cropper-cover-area';
   private cropInfoSelector: string = '.t3js-cropper-info-crop';
   private focusAreaSelector: string = '#t3js-cropper-focus-area';
@@ -173,6 +175,7 @@ class ImageManipulation {
     viewMode: 1,
     zoomable: false,
   };
+  private resizeTimeout: number = 450;
 
   constructor() {
     // Silence is golden
@@ -204,10 +207,12 @@ class ImageManipulation {
   }
 
   /**
-   * Initialize the cropper modal
+   * @method initializeCropperModal
+   * @desc Initialize the cropper modal and dispatch the cropper init
+   * @private
    */
   private initializeCropperModal(): void {
-    const image: JQuery = this.currentModal.find('#t3js-crop-image');
+    const image: JQuery = this.currentModal.find(this.cropImageSelector);
     ImagesLoaded(image, (): void => {
       const modal: JQuery = this.currentModal.find('.modal-dialog');
       modal.css({marginLeft: 'auto', marginRight: 'auto'});
@@ -219,6 +224,11 @@ class ImageManipulation {
     });
   }
 
+  /**
+   * @method show
+   * @desc Load the image and setup the modal UI
+   * @private
+   */
   private show(): void {
     const modalTitle: string = this.trigger.data('modalTitle');
     const imageUri: string = this.trigger.data('url');
@@ -236,10 +246,20 @@ class ImageManipulation {
       '.modal-content'
     );
     this.currentModal.addClass('modal-dark');
+    this.currentModal.on('hide.bs.modal', (e: JQueryEventObject): void => {
+      this.destroy();
+    });
+    // Do not dismiss the modal when clicking beside it to avoid data loss
+    this.currentModal.data('bs.modal').options.backdrop = 'static';
   }
 
+  /**
+   * @method init
+   * @desc Initializes the cropper UI and sets up all the event indings for the UI
+   * @private
+   */
   private init(): void {
-    const image: JQuery = this.currentModal.find('#t3js-crop-image');
+    const image: JQuery = this.currentModal.find(this.cropImageSelector);
     const imageHeight: number = $(image).height();
     const imageWidth: number = $(image).width();
     const data: string = this.trigger.attr('data-crop-variants');
@@ -251,7 +271,7 @@ class ImageManipulation {
     // If we have data already set we assume an internal reinit eg. after resizing
     this.data = $.isEmptyObject(this.data) ? JSON.parse(data) : this.data;
     // Initialize our class members
-    this.currentModal.find('.cropper-image-container').css({height: imageHeight, width: imageWidth});
+    this.currentModal.find(this.cropImageContainerSelector).css({height: imageHeight, width: imageWidth});
     this.cropVariantTriggers = this.currentModal.find('.t3js-crop-variant-trigger');
     this.activeCropVariantTrigger = this.currentModal.find('.t3js-crop-variant-trigger.is-active');
     this.cropInfo = this.currentModal.find(this.cropInfoSelector);
@@ -266,7 +286,7 @@ class ImageManipulation {
     /**
      * Assign EventListener to cropVariantTriggers
      */
-    this.cropVariantTriggers.on('click', (e: JQueryEventObject): void => {
+    this.cropVariantTriggers.off('click').on('click', (e: JQueryEventObject): void => {
 
       /**
        * Is the current cropVariantTrigger is active, bail out.
@@ -291,12 +311,12 @@ class ImageManipulation {
     /**
      * Assign EventListener to aspectRatioTrigger
      */
-    this.aspectRatioTrigger.on('click', (e: JQueryEventObject): void => {
+    this.aspectRatioTrigger.off('click').on('click', (e: JQueryEventObject): void => {
       const ratioId: string = $(e.currentTarget).attr('data-option');
       const temp: CropVariant = $.extend(true, {}, this.currentCropVariant);
       const ratio: Ratio = temp.allowedAspectRatios[ratioId];
-      this.updateAspectRatio(ratio);
-      // Set data explicitly or updateAspectRatio upscales the crop
+      this.setAspectRatio(ratio);
+      // Set data explicitly or setAspectRatio upscales the crop
       this.setCropArea(temp.cropArea);
       this.currentCropVariant = $.extend(true, {}, temp, {selectedRatio: ratioId});
       this.update(this.currentCropVariant);
@@ -305,7 +325,7 @@ class ImageManipulation {
     /**
      * Assign EventListener to saveButton
      */
-    this.saveButton.on('click', (): void => {
+    this.saveButton.off('click').on('click', (): void => {
       this.save(this.data);
     });
 
@@ -313,7 +333,7 @@ class ImageManipulation {
      * Assign EventListener to previewButton if preview url exists
      */
     if (this.trigger.attr('data-preview-url')) {
-      this.previewButton.on('click', (): void => {
+      this.previewButton.off('click').on('click', (): void => {
         this.openPreview(this.data);
       });
     } else {
@@ -323,14 +343,14 @@ class ImageManipulation {
     /**
      * Assign EventListener to dismissButton
      */
-    this.dismissButton.on('click', (): void => {
-      this.destroy();
+    this.dismissButton.off('click').on('click', (): void => {
+      this.currentModal.modal('hide');
     });
 
     /**
      * Assign EventListener to resetButton
      */
-    this.resetButton.on('click', (e: JQueryEventObject): void => {
+    this.resetButton.off('click').on('click', (e: JQueryEventObject): void => {
       const imageData: CropperImageData = this.cropper.cropper('getImageData');
       const resetCropVariantString: string = $(e.currentTarget).attr('data-crop-variant');
       e.preventDefault();
@@ -367,12 +387,30 @@ class ImageManipulation {
     }));
   }
 
+  /**
+   * @method cropBuiltHandler
+   * @desc Internal cropper handler. Called when the cropper has been instantiated
+   * @private
+   */
   private cropBuiltHandler = (): void => {
     const imageData: CropperImageData = this.cropper.cropper('getImageData');
+
+    // Iterate over the crop variants and set up their respective preview
+    this.cropVariantTriggers.each((index: number, elem: Element): void => {
+      const cropVariantId: string = $(elem).attr('data-crop-variant-id');
+      const cropArea: Area = this.convertRelativeToAbsoluteCropArea(
+        this.data[cropVariantId].cropArea,
+        imageData
+      );
+      const variant: CropVariant = $.extend(true, {}, this.data[cropVariantId], {cropArea});
+      this.updatePreviewThumbnail(variant, $(elem));
+    });
+
     this.currentCropVariant.cropArea = this.convertRelativeToAbsoluteCropArea(
       this.currentCropVariant.cropArea,
       imageData
     );
+    // Can't use .t3js-* as selector because it is an extraneous selector
     this.cropBox = this.currentModal.find('.cropper-crop-box');
 
     this.setCropArea(this.currentCropVariant.cropArea);
@@ -394,14 +432,20 @@ class ImageManipulation {
     }
 
     if (this.currentCropVariant.selectedRatio) {
-      this.updateAspectRatio(this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio]);
-      // Set data explicitly or updateAspectRatio up-scales the crop
+      this.setAspectRatio(this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio]);
+      // Set data explicitly or setAspectRatio up-scales the crop
       this.setCropArea(this.currentCropVariant.cropArea);
       this.currentModal.find(`[data-option='${this.currentCropVariant.selectedRatio}']`).addClass('active');
     }
+
     this.cropperCanvas.addClass('is-visible');
   };
 
+  /**
+   * @method cropMoveHandler
+   * @desc Internal cropper handler. Called when the cropping area is moving
+   * @private
+   */
   private cropMoveHandler = (e: CropperEvent): void => {
     this.currentCropVariant.cropArea = $.extend(true, this.currentCropVariant.cropArea, {
       height: Math.floor(e.height),
@@ -409,11 +453,16 @@ class ImageManipulation {
       x: Math.floor(e.x),
       y: Math.floor(e.y),
     });
-    this.updatePreviewThumbnail(this.currentCropVariant);
+    this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
     this.updateCropVariantData(this.currentCropVariant);
     this.cropInfo.text(`${this.currentCropVariant.cropArea.width}×${this.currentCropVariant.cropArea.height} px`);
   };
 
+  /**
+   * @method cropStartHandler
+   * @desc Internal cropper handler. Called when the cropping starts moving
+   * @private
+   */
   private cropStartHandler = (): void => {
     if (this.currentCropVariant.focusArea) {
       this.focusArea.draggable('option', 'disabled', true);
@@ -422,7 +471,9 @@ class ImageManipulation {
   };
 
   /**
-   *
+   * @method cropEndHandler
+   * @desc Internal cropper handler. Called when the cropping ends moving
+   * @private
    */
   private cropEndHandler = (): void => {
     if (this.currentCropVariant.focusArea) {
@@ -444,7 +495,7 @@ class ImageManipulation {
     /**
      * Setting the aspect ratio cause a redraw of the crop area so we need to manually reset it to last data
      */
-    this.updateAspectRatio(selectedRatio);
+    this.setAspectRatio(selectedRatio);
     this.setCropArea(temp.cropArea);
     this.currentCropVariant = $.extend(true, {}, temp, cropVariant);
     this.cropBox.find(this.coverAreaSelector).remove();
@@ -470,13 +521,14 @@ class ImageManipulation {
       // Init or reinit focusArea
       this.initCoverAreas(this.cropBox, this.currentCropVariant.coverAreas);
     }
-    this.updatePreviewThumbnail(this.currentCropVariant);
+    this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
   }
 
   /**
    * @method initFocusArea
    * @desc Initializes the focus area inside a container and registers the resizable and draggable interfaces to it
-   * @param container: JQuery
+   * @param {JQuery} container
+   * @private
    */
   private initFocusArea(container: JQuery): void {
     this.focusArea = $('<div id="t3js-cropper-focus-area" class="cropper-focus-area"></div>');
@@ -494,7 +546,7 @@ class ImageManipulation {
 
           focusArea.x = (fLeft - left) / container.width();
           focusArea.y = (fTop - top) / container.height();
-          this.updatePreviewThumbnail(this.currentCropVariant);
+          this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
           if (this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
             this.focusArea.addClass('has-nodrop');
           } else {
@@ -541,7 +593,7 @@ class ImageManipulation {
           focusArea.width = this.focusArea.width() / container.width();
           focusArea.x = (fLeft - left) / container.width();
           focusArea.y = (fTop - top) / container.height();
-          this.updatePreviewThumbnail(this.currentCropVariant);
+          this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
 
           if (this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
             this.focusArea.addClass('has-nodrop');
@@ -597,16 +649,18 @@ class ImageManipulation {
   /**
    * @method updatePreviewThumbnail
    * @desc Sync the croping (and focus area) to the preview thumbnail
-   * @param {CropVariant} cropVariant
+   * @param {CropVariant} cropVariant - The crop variant to preview in the thumbnail
+   * @param {JQuery} cropVariantTrigger - The crop variant element containing the thumbnail
+   * @private
    */
-  private updatePreviewThumbnail(cropVariant: CropVariant): void {
+  private updatePreviewThumbnail(cropVariant: CropVariant, cropVariantTrigger: JQuery): void {
     let styles: any;
     const cropperPreviewThumbnailCrop: JQuery =
-      this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-area');
+      cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-area');
     const cropperPreviewThumbnailImage: JQuery =
-      this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-image');
+      cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-image');
     const cropperPreviewThumbnailFocus: JQuery =
-      this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-focus-area');
+      cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-focus-area');
     const imageData: CropperImageData = this.cropper.cropper('getImageData');
 
     // Update the position/dimension of the crop area in the preview
@@ -657,7 +711,7 @@ class ImageManipulation {
       width: ImageManipulation.toCssPercent(focusArea.width),
     });
     this.currentCropVariant.focusArea = focusArea;
-    this.updatePreviewThumbnail(this.currentCropVariant);
+    this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
     this.updateCropVariantData(this.currentCropVariant);
   }
 
@@ -665,6 +719,7 @@ class ImageManipulation {
    * @method updateCropVariantData
    * @desc Immutably updates the currently selected cropVariant data
    * @param {CropVariant} currentCropVariant - The cropVariant to immutably save
+   * @private
    */
   private updateCropVariantData(currentCropVariant: CropVariant): void {
     const imageData: CropperImageData = this.cropper.cropper('getImageData');
@@ -673,26 +728,37 @@ class ImageManipulation {
   }
 
   /**
-   * @method updateAspectRatio
-   * @desc Updates the aspect ratio in the cropper
-   * @param {ratio} ratio ratio set in the cropper
+   * @method setAspectRatio
+   * @desc Sets the cropper to a specific ratio
+   * @param {ratio} ratio - The ratio value to apply
+   * @private
    */
-  private updateAspectRatio(ratio: Ratio): void {
+  private setAspectRatio(ratio: Ratio): void {
     this.cropper.cropper('setAspectRatio', ratio.value);
   }
 
   /**
    * @method setCropArea
-   * @desc Updates the crop area in the cropper. The cropper will respect the selected ratio
-   * @param {cropArea} cropArea ratio set in the cropper
+   * @desc Sets the cropper to a specific crop area
+   * @param {cropArea} cropArea - The crop area to apply
+   * @private
    */
   private setCropArea(cropArea: Area): void {
-    this.cropper.cropper('setData', {
-      height: cropArea.height,
-      width: cropArea.width,
-      x: cropArea.x,
-      y: cropArea.y,
-    });
+    const currentRatio: Ratio = this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio];
+    if (currentRatio.value === 0) {
+      this.cropper.cropper('setData', {
+        height: cropArea.height,
+        width: cropArea.width,
+        x: cropArea.x,
+        y: cropArea.y,
+      });
+    } else {
+      this.cropper.cropper('setData', {
+        height: cropArea.height,
+        x: cropArea.x,
+        y: cropArea.y,
+      });
+    }
   }
 
   /**
@@ -716,9 +782,11 @@ class ImageManipulation {
   }
 
   /**
-   * @param cropArea
-   * @param imageData
-   * @return {{height: number, width: number, x: number, y: number}}
+   * @method convertAbsoluteToRelativeCropArea
+   * @desc Converts a crop area from absolute pixel-based into relative length values
+   * @param {Area} cropArea - The crop area to convert from
+   * @param {CropperImageData} imageData - The image data
+   * @return {Area}
    */
   private convertAbsoluteToRelativeCropArea(cropArea: Area, imageData: CropperImageData): Area {
     const {height, width, x, y}: Area = cropArea;
@@ -731,8 +799,10 @@ class ImageManipulation {
   }
 
   /**
-   * @param cropArea
-   * @param imageData
+   * @method convertRelativeToAbsoluteCropArea
+   * @desc Converts a crop area from relative into absolute pixel-based length values
+   * @param {Area} cropArea - The crop area to convert from
+   * @param {CropperImageData} imageData - The image data
    * @return {{height: number, width: number, x: number, y: number}}
    */
   private convertRelativeToAbsoluteCropArea(cropArea: Area, imageData: CropperImageData): Area {
@@ -745,9 +815,16 @@ class ImageManipulation {
     };
   }
 
-  private setPreviewImage(data: Object): void {
+  /**
+   * @method setPreviewImages
+   * @desc Updates the preview images in the editing section with the respective crop variants
+   * @param {Object} data - The internal crop variants state
+   */
+  private setPreviewImages(data: Object): void {
     let $image: any = this.cropper;
     let imageData: CropperImageData = $image.cropper('getImageData');
+
+    // Iterate over the crop variants and set up their respective preview
     Object.keys(data).forEach((cropVariantId: string) => {
       const cropVariant: CropVariant = data[cropVariantId];
       const cropData: Area = this.convertRelativeToAbsoluteCropArea(cropVariant.cropArea, imageData);
@@ -795,7 +872,7 @@ class ImageManipulation {
 
   /**
    * @method openPreview
-   * @desc open a preview
+   * @desc Opens a preview view with the crop variants
    * @param {object} data - The whole data object containing all the cropVariants
    * @private
    */
@@ -816,9 +893,9 @@ class ImageManipulation {
     const cropVariants: string = ImageManipulation.serializeCropVariants(data);
     const hiddenField: JQuery = $(`#${this.trigger.attr('data-field')}`);
     this.trigger.attr('data-crop-variants', JSON.stringify(data));
-    this.setPreviewImage(data);
+    this.setPreviewImages(data);
     hiddenField.val(cropVariants);
-    this.destroy();
+    this.currentModal.modal('hide');
   }
 
   /**
@@ -828,19 +905,26 @@ class ImageManipulation {
    */
   private destroy(): void {
     if (this.currentModal) {
-      this.currentModal.modal('hide');
       this.cropper.cropper('destroy');
+      this.cropper = null;
       this.currentModal = null;
+      this.data = null;
     }
   }
 
+  /**
+   * @method resizeEnd
+   * @desc Calls a function when the cropper has been resized
+   * @param {Function} fn - The function to call on resize completion
+   * @private
+   */
   private resizeEnd(fn: Function): void {
     let timer: number;
     $(window).on('resize', (): void => {
       clearTimeout(timer);
       timer = setTimeout((): void => {
         fn();
-      }, 450);
+      }, this.resizeTimeout);
     });
   }
 }
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/ImageManipulation.js b/typo3/sysext/backend/Resources/Public/JavaScript/ImageManipulation.js
index fd6907c5046ce9a49f080ec6f0d933d0c06d610b..25ea3324f8a45cab4d269e3053f90218d1488173 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/ImageManipulation.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/ImageManipulation.js
@@ -20,6 +20,8 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
     var ImageManipulation = (function () {
         function ImageManipulation() {
             var _this = this;
+            this.cropImageContainerSelector = '#t3js-crop-image-container';
+            this.cropImageSelector = '#t3js-crop-image';
             this.coverAreaSelector = '.t3js-cropper-cover-area';
             this.cropInfoSelector = '.t3js-cropper-info-crop';
             this.focusAreaSelector = '#t3js-cropper-focus-area';
@@ -38,9 +40,23 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
                 viewMode: 1,
                 zoomable: false,
             };
+            this.resizeTimeout = 450;
+            /**
+             * @method cropBuiltHandler
+             * @desc Internal cropper handler. Called when the cropper has been instantiated
+             * @private
+             */
             this.cropBuiltHandler = function () {
                 var imageData = _this.cropper.cropper('getImageData');
+                // Iterate over the crop variants and set up their respective preview
+                _this.cropVariantTriggers.each(function (index, elem) {
+                    var cropVariantId = $(elem).attr('data-crop-variant-id');
+                    var cropArea = _this.convertRelativeToAbsoluteCropArea(_this.data[cropVariantId].cropArea, imageData);
+                    var variant = $.extend(true, {}, _this.data[cropVariantId], { cropArea: cropArea });
+                    _this.updatePreviewThumbnail(variant, $(elem));
+                });
                 _this.currentCropVariant.cropArea = _this.convertRelativeToAbsoluteCropArea(_this.currentCropVariant.cropArea, imageData);
+                // Can't use .t3js-* as selector because it is an extraneous selector
                 _this.cropBox = _this.currentModal.find('.cropper-crop-box');
                 _this.setCropArea(_this.currentCropVariant.cropArea);
                 // Check if new cropVariant has coverAreas
@@ -59,13 +75,18 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
                     _this.scaleAndMoveFocusArea(_this.currentCropVariant.focusArea);
                 }
                 if (_this.currentCropVariant.selectedRatio) {
-                    _this.updateAspectRatio(_this.currentCropVariant.allowedAspectRatios[_this.currentCropVariant.selectedRatio]);
-                    // Set data explicitly or updateAspectRatio up-scales the crop
+                    _this.setAspectRatio(_this.currentCropVariant.allowedAspectRatios[_this.currentCropVariant.selectedRatio]);
+                    // Set data explicitly or setAspectRatio up-scales the crop
                     _this.setCropArea(_this.currentCropVariant.cropArea);
                     _this.currentModal.find("[data-option='" + _this.currentCropVariant.selectedRatio + "']").addClass('active');
                 }
                 _this.cropperCanvas.addClass('is-visible');
             };
+            /**
+             * @method cropMoveHandler
+             * @desc Internal cropper handler. Called when the cropping area is moving
+             * @private
+             */
             this.cropMoveHandler = function (e) {
                 _this.currentCropVariant.cropArea = $.extend(true, _this.currentCropVariant.cropArea, {
                     height: Math.floor(e.height),
@@ -73,10 +94,15 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
                     x: Math.floor(e.x),
                     y: Math.floor(e.y),
                 });
-                _this.updatePreviewThumbnail(_this.currentCropVariant);
+                _this.updatePreviewThumbnail(_this.currentCropVariant, _this.activeCropVariantTrigger);
                 _this.updateCropVariantData(_this.currentCropVariant);
                 _this.cropInfo.text(_this.currentCropVariant.cropArea.width + "\u00D7" + _this.currentCropVariant.cropArea.height + " px");
             };
+            /**
+             * @method cropStartHandler
+             * @desc Internal cropper handler. Called when the cropping starts moving
+             * @private
+             */
             this.cropStartHandler = function () {
                 if (_this.currentCropVariant.focusArea) {
                     _this.focusArea.draggable('option', 'disabled', true);
@@ -84,7 +110,9 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
                 }
             };
             /**
-             *
+             * @method cropEndHandler
+             * @desc Internal cropper handler. Called when the cropping ends moving
+             * @private
              */
             this.cropEndHandler = function () {
                 if (_this.currentCropVariant.focusArea) {
@@ -168,11 +196,13 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
             $('.t3js-image-manipulation-trigger').off('click').click(triggerHandler);
         };
         /**
-         * Initialize the cropper modal
+         * @method initializeCropperModal
+         * @desc Initialize the cropper modal and dispatch the cropper init
+         * @private
          */
         ImageManipulation.prototype.initializeCropperModal = function () {
             var _this = this;
-            var image = this.currentModal.find('#t3js-crop-image');
+            var image = this.currentModal.find(this.cropImageSelector);
             ImagesLoaded(image, function () {
                 var modal = _this.currentModal.find('.modal-dialog');
                 modal.css({ marginLeft: 'auto', marginRight: 'auto' });
@@ -183,7 +213,13 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
                 }, 100);
             });
         };
+        /**
+         * @method show
+         * @desc Load the image and setup the modal UI
+         * @private
+         */
         ImageManipulation.prototype.show = function () {
+            var _this = this;
             var modalTitle = this.trigger.data('modalTitle');
             var imageUri = this.trigger.data('url');
             var initCropperModal = this.initializeCropperModal.bind(this);
@@ -192,10 +228,20 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
              */
             this.currentModal = Modal.loadUrl(modalTitle, Severity.notice, [], imageUri, initCropperModal, '.modal-content');
             this.currentModal.addClass('modal-dark');
+            this.currentModal.on('hide.bs.modal', function (e) {
+                _this.destroy();
+            });
+            // Do not dismiss the modal when clicking beside it to avoid data loss
+            this.currentModal.data('bs.modal').options.backdrop = 'static';
         };
+        /**
+         * @method init
+         * @desc Initializes the cropper UI and sets up all the event indings for the UI
+         * @private
+         */
         ImageManipulation.prototype.init = function () {
             var _this = this;
-            var image = this.currentModal.find('#t3js-crop-image');
+            var image = this.currentModal.find(this.cropImageSelector);
             var imageHeight = $(image).height();
             var imageWidth = $(image).width();
             var data = this.trigger.attr('data-crop-variants');
@@ -205,7 +251,7 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
             // If we have data already set we assume an internal reinit eg. after resizing
             this.data = $.isEmptyObject(this.data) ? JSON.parse(data) : this.data;
             // Initialize our class members
-            this.currentModal.find('.cropper-image-container').css({ height: imageHeight, width: imageWidth });
+            this.currentModal.find(this.cropImageContainerSelector).css({ height: imageHeight, width: imageWidth });
             this.cropVariantTriggers = this.currentModal.find('.t3js-crop-variant-trigger');
             this.activeCropVariantTrigger = this.currentModal.find('.t3js-crop-variant-trigger.is-active');
             this.cropInfo = this.currentModal.find(this.cropInfoSelector);
@@ -219,7 +265,7 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
             /**
              * Assign EventListener to cropVariantTriggers
              */
-            this.cropVariantTriggers.on('click', function (e) {
+            this.cropVariantTriggers.off('click').on('click', function (e) {
                 /**
                  * Is the current cropVariantTrigger is active, bail out.
                  * Bootstrap doesn't provide this functionality when collapsing the Collaps panels
@@ -241,12 +287,12 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
             /**
              * Assign EventListener to aspectRatioTrigger
              */
-            this.aspectRatioTrigger.on('click', function (e) {
+            this.aspectRatioTrigger.off('click').on('click', function (e) {
                 var ratioId = $(e.currentTarget).attr('data-option');
                 var temp = $.extend(true, {}, _this.currentCropVariant);
                 var ratio = temp.allowedAspectRatios[ratioId];
-                _this.updateAspectRatio(ratio);
-                // Set data explicitly or updateAspectRatio upscales the crop
+                _this.setAspectRatio(ratio);
+                // Set data explicitly or setAspectRatio upscales the crop
                 _this.setCropArea(temp.cropArea);
                 _this.currentCropVariant = $.extend(true, {}, temp, { selectedRatio: ratioId });
                 _this.update(_this.currentCropVariant);
@@ -254,14 +300,14 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
             /**
              * Assign EventListener to saveButton
              */
-            this.saveButton.on('click', function () {
+            this.saveButton.off('click').on('click', function () {
                 _this.save(_this.data);
             });
             /**
              * Assign EventListener to previewButton if preview url exists
              */
             if (this.trigger.attr('data-preview-url')) {
-                this.previewButton.on('click', function () {
+                this.previewButton.off('click').on('click', function () {
                     _this.openPreview(_this.data);
                 });
             }
@@ -271,13 +317,13 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
             /**
              * Assign EventListener to dismissButton
              */
-            this.dismissButton.on('click', function () {
-                _this.destroy();
+            this.dismissButton.off('click').on('click', function () {
+                _this.currentModal.modal('hide');
             });
             /**
              * Assign EventListener to resetButton
              */
-            this.resetButton.on('click', function (e) {
+            this.resetButton.off('click').on('click', function (e) {
                 var imageData = _this.cropper.cropper('getImageData');
                 var resetCropVariantString = $(e.currentTarget).attr('data-crop-variant');
                 e.preventDefault();
@@ -324,7 +370,7 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
             /**
              * Setting the aspect ratio cause a redraw of the crop area so we need to manually reset it to last data
              */
-            this.updateAspectRatio(selectedRatio);
+            this.setAspectRatio(selectedRatio);
             this.setCropArea(temp.cropArea);
             this.currentCropVariant = $.extend(true, {}, temp, cropVariant);
             this.cropBox.find(this.coverAreaSelector).remove();
@@ -347,12 +393,13 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
                 // Init or reinit focusArea
                 this.initCoverAreas(this.cropBox, this.currentCropVariant.coverAreas);
             }
-            this.updatePreviewThumbnail(this.currentCropVariant);
+            this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
         };
         /**
          * @method initFocusArea
          * @desc Initializes the focus area inside a container and registers the resizable and draggable interfaces to it
-         * @param container: JQuery
+         * @param {JQuery} container
+         * @private
          */
         ImageManipulation.prototype.initFocusArea = function (container) {
             var _this = this;
@@ -370,7 +417,7 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
                     var _c = _this.currentCropVariant, focusArea = _c.focusArea, coverAreas = _c.coverAreas;
                     focusArea.x = (fLeft - left) / container.width();
                     focusArea.y = (fTop - top) / container.height();
-                    _this.updatePreviewThumbnail(_this.currentCropVariant);
+                    _this.updatePreviewThumbnail(_this.currentCropVariant, _this.activeCropVariantTrigger);
                     if (_this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
                         _this.focusArea.addClass('has-nodrop');
                     }
@@ -414,7 +461,7 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
                     focusArea.width = _this.focusArea.width() / container.width();
                     focusArea.x = (fLeft - left) / container.width();
                     focusArea.y = (fTop - top) / container.height();
-                    _this.updatePreviewThumbnail(_this.currentCropVariant);
+                    _this.updatePreviewThumbnail(_this.currentCropVariant, _this.activeCropVariantTrigger);
                     if (_this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
                         _this.focusArea.addClass('has-nodrop');
                     }
@@ -465,13 +512,15 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
         /**
          * @method updatePreviewThumbnail
          * @desc Sync the croping (and focus area) to the preview thumbnail
-         * @param {CropVariant} cropVariant
+         * @param {CropVariant} cropVariant - The crop variant to preview in the thumbnail
+         * @param {JQuery} cropVariantTrigger - The crop variant element containing the thumbnail
+         * @private
          */
-        ImageManipulation.prototype.updatePreviewThumbnail = function (cropVariant) {
+        ImageManipulation.prototype.updatePreviewThumbnail = function (cropVariant, cropVariantTrigger) {
             var styles;
-            var cropperPreviewThumbnailCrop = this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-area');
-            var cropperPreviewThumbnailImage = this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-image');
-            var cropperPreviewThumbnailFocus = this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-focus-area');
+            var cropperPreviewThumbnailCrop = cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-area');
+            var cropperPreviewThumbnailImage = cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-image');
+            var cropperPreviewThumbnailFocus = cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-focus-area');
             var imageData = this.cropper.cropper('getImageData');
             // Update the position/dimension of the crop area in the preview
             cropperPreviewThumbnailCrop.css({
@@ -517,13 +566,14 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
                 width: ImageManipulation.toCssPercent(focusArea.width),
             });
             this.currentCropVariant.focusArea = focusArea;
-            this.updatePreviewThumbnail(this.currentCropVariant);
+            this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
             this.updateCropVariantData(this.currentCropVariant);
         };
         /**
          * @method updateCropVariantData
          * @desc Immutably updates the currently selected cropVariant data
          * @param {CropVariant} currentCropVariant - The cropVariant to immutably save
+         * @private
          */
         ImageManipulation.prototype.updateCropVariantData = function (currentCropVariant) {
             var imageData = this.cropper.cropper('getImageData');
@@ -531,25 +581,37 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
             this.data[currentCropVariant.id] = $.extend(true, {}, currentCropVariant, { cropArea: absoluteCropArea });
         };
         /**
-         * @method updateAspectRatio
-         * @desc Updates the aspect ratio in the cropper
-         * @param {ratio} ratio ratio set in the cropper
+         * @method setAspectRatio
+         * @desc Sets the cropper to a specific ratio
+         * @param {ratio} ratio - The ratio value to apply
+         * @private
          */
-        ImageManipulation.prototype.updateAspectRatio = function (ratio) {
+        ImageManipulation.prototype.setAspectRatio = function (ratio) {
             this.cropper.cropper('setAspectRatio', ratio.value);
         };
         /**
          * @method setCropArea
-         * @desc Updates the crop area in the cropper. The cropper will respect the selected ratio
-         * @param {cropArea} cropArea ratio set in the cropper
+         * @desc Sets the cropper to a specific crop area
+         * @param {cropArea} cropArea - The crop area to apply
+         * @private
          */
         ImageManipulation.prototype.setCropArea = function (cropArea) {
-            this.cropper.cropper('setData', {
-                height: cropArea.height,
-                width: cropArea.width,
-                x: cropArea.x,
-                y: cropArea.y,
-            });
+            var currentRatio = this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio];
+            if (currentRatio.value === 0) {
+                this.cropper.cropper('setData', {
+                    height: cropArea.height,
+                    width: cropArea.width,
+                    x: cropArea.x,
+                    y: cropArea.y,
+                });
+            }
+            else {
+                this.cropper.cropper('setData', {
+                    height: cropArea.height,
+                    x: cropArea.x,
+                    y: cropArea.y,
+                });
+            }
         };
         /**
          * @method checkFocusAndCoverAreas
@@ -571,9 +633,11 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
             });
         };
         /**
-         * @param cropArea
-         * @param imageData
-         * @return {{height: number, width: number, x: number, y: number}}
+         * @method convertAbsoluteToRelativeCropArea
+         * @desc Converts a crop area from absolute pixel-based into relative length values
+         * @param {Area} cropArea - The crop area to convert from
+         * @param {CropperImageData} imageData - The image data
+         * @return {Area}
          */
         ImageManipulation.prototype.convertAbsoluteToRelativeCropArea = function (cropArea, imageData) {
             var height = cropArea.height, width = cropArea.width, x = cropArea.x, y = cropArea.y;
@@ -585,8 +649,10 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
             };
         };
         /**
-         * @param cropArea
-         * @param imageData
+         * @method convertRelativeToAbsoluteCropArea
+         * @desc Converts a crop area from relative into absolute pixel-based length values
+         * @param {Area} cropArea - The crop area to convert from
+         * @param {CropperImageData} imageData - The image data
          * @return {{height: number, width: number, x: number, y: number}}
          */
         ImageManipulation.prototype.convertRelativeToAbsoluteCropArea = function (cropArea, imageData) {
@@ -598,10 +664,16 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
                 y: y * imageData.naturalHeight,
             };
         };
-        ImageManipulation.prototype.setPreviewImage = function (data) {
+        /**
+         * @method setPreviewImages
+         * @desc Updates the preview images in the editing section with the respective crop variants
+         * @param {Object} data - The internal crop variants state
+         */
+        ImageManipulation.prototype.setPreviewImages = function (data) {
             var _this = this;
             var $image = this.cropper;
             var imageData = $image.cropper('getImageData');
+            // Iterate over the crop variants and set up their respective preview
             Object.keys(data).forEach(function (cropVariantId) {
                 var cropVariant = data[cropVariantId];
                 var cropData = _this.convertRelativeToAbsoluteCropArea(cropVariant.cropArea, imageData);
@@ -643,7 +715,7 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
         ;
         /**
          * @method openPreview
-         * @desc open a preview
+         * @desc Opens a preview view with the crop variants
          * @param {object} data - The whole data object containing all the cropVariants
          * @private
          */
@@ -663,9 +735,9 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
             var cropVariants = ImageManipulation.serializeCropVariants(data);
             var hiddenField = $("#" + this.trigger.attr('data-field'));
             this.trigger.attr('data-crop-variants', JSON.stringify(data));
-            this.setPreviewImage(data);
+            this.setPreviewImages(data);
             hiddenField.val(cropVariants);
-            this.destroy();
+            this.currentModal.modal('hide');
         };
         /**
          * @method destroy
@@ -674,18 +746,26 @@ define(["require", "exports", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "T
          */
         ImageManipulation.prototype.destroy = function () {
             if (this.currentModal) {
-                this.currentModal.modal('hide');
                 this.cropper.cropper('destroy');
+                this.cropper = null;
                 this.currentModal = null;
+                this.data = null;
             }
         };
+        /**
+         * @method resizeEnd
+         * @desc Calls a function when the cropper has been resized
+         * @param {Function} fn - The function to call on resize completion
+         * @private
+         */
         ImageManipulation.prototype.resizeEnd = function (fn) {
+            var _this = this;
             var timer;
             $(window).on('resize', function () {
                 clearTimeout(timer);
                 timer = setTimeout(function () {
                     fn();
-                }, 450);
+                }, _this.resizeTimeout);
             });
         };
         return ImageManipulation;
diff --git a/typo3/sysext/core/Classes/Imaging/ImageManipulation/Area.php b/typo3/sysext/core/Classes/Imaging/ImageManipulation/Area.php
index e74af9bebecc4c890cbdff9e3977241fdfd8565f..54388dd5e5c22021cc19b3953b287ca76ea4ed9f 100644
--- a/typo3/sysext/core/Classes/Imaging/ImageManipulation/Area.php
+++ b/typo3/sysext/core/Classes/Imaging/ImageManipulation/Area.php
@@ -136,6 +136,27 @@ class Area
         );
     }
 
+    /**
+     * @param Ratio $ratio
+     * @return Area
+     */
+    public function applyRatioRestriction(Ratio $ratio): Area
+    {
+        if ($ratio->isFree()) {
+            return $this;
+        }
+        $expectedRatio = $ratio->getRatioValue();
+        $newArea = clone $this;
+        if ($newArea->height * $expectedRatio > $newArea->width) {
+            $newArea->height = $newArea->width / $expectedRatio;
+            $newArea->y += ($this->height - $newArea->height) / 2;
+        } else {
+            $newArea->width = $newArea->height * $expectedRatio;
+            $newArea->x += ($this->width - $newArea->width) / 2;
+        }
+        return $newArea;
+    }
+
     /**
      * @return bool
      */
diff --git a/typo3/sysext/core/Classes/Imaging/ImageManipulation/CropVariant.php b/typo3/sysext/core/Classes/Imaging/ImageManipulation/CropVariant.php
index f84ec4b13a634669001ff9e644c3ade6269ffed1..1645148ad7b5656b83bb6be226c72dc26b9fa787 100644
--- a/typo3/sysext/core/Classes/Imaging/ImageManipulation/CropVariant.php
+++ b/typo3/sysext/core/Classes/Imaging/ImageManipulation/CropVariant.php
@@ -15,6 +15,8 @@ namespace TYPO3\CMS\Core\Imaging\ImageManipulation;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Resource\FileInterface;
+
 class CropVariant
 {
     /**
@@ -76,8 +78,12 @@ class CropVariant
         $this->cropArea = $cropArea;
         if ($allowedAspectRatios) {
             $this->setAllowedAspectRatios(...$allowedAspectRatios);
+            if ($selectedRatio && isset($this->allowedAspectRatios[$selectedRatio])) {
+                $this->selectedRatio = $selectedRatio;
+            } else {
+                $this->selectedRatio = current($this->allowedAspectRatios)->getId();
+            }
         }
-        $this->selectedRatio = $selectedRatio;
         $this->focusArea = $focusArea;
         if ($coverAreas !== null) {
             $this->setCoverAreas(...$coverAreas);
@@ -158,6 +164,22 @@ class CropVariant
         return $this->focusArea;
     }
 
+    /**
+     * @param FileInterface $file
+     * @return CropVariant
+     */
+    public function applyRatioRestrictionToSelectedCropArea(FileInterface $file): CropVariant
+    {
+        if (!$this->selectedRatio) {
+            return $this;
+        }
+        $newVariant = clone $this;
+        $newArea = $this->cropArea->makeAbsoluteBasedOnFile($file);
+        $newArea = $newArea->applyRatioRestriction($this->allowedAspectRatios[$this->selectedRatio]);
+        $newVariant->cropArea = $newArea->makeRelativeBasedOnFile($file);
+        return $newVariant;
+    }
+
     /**
      * @param Ratio[] $ratios
      * @throws InvalidConfigurationException
diff --git a/typo3/sysext/core/Classes/Imaging/ImageManipulation/CropVariantCollection.php b/typo3/sysext/core/Classes/Imaging/ImageManipulation/CropVariantCollection.php
index c60b8681e4e74f96379a82d1e38f1d08f773ac95..7327a68727a8720a3caf336a578487c8cee939e6 100644
--- a/typo3/sysext/core/Classes/Imaging/ImageManipulation/CropVariantCollection.php
+++ b/typo3/sysext/core/Classes/Imaging/ImageManipulation/CropVariantCollection.php
@@ -15,6 +15,8 @@ namespace TYPO3\CMS\Core\Imaging\ImageManipulation;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Resource\FileInterface;
+
 class CropVariantCollection
 {
     /**
@@ -38,17 +40,17 @@ class CropVariantCollection
      */
     public static function create(string $jsonString, array $tcaConfig = []): CropVariantCollection
     {
-        if (empty($jsonString) && empty($tcaConfig)) {
+        $persistedCollectionConfig = empty($jsonString) ? [] : json_decode($jsonString, true);
+        if (empty($persistedCollectionConfig) && empty($tcaConfig)) {
             return self::createEmpty();
         }
-        $persistedCollectionConfig = json_decode($jsonString, true);
-        if (!is_array($persistedCollectionConfig)) {
-            $persistedCollectionConfig = [];
-        }
         try {
             if ($tcaConfig === []) {
-                $tcaConfig = $persistedCollectionConfig;
+                $tcaConfig = (array)$persistedCollectionConfig;
             } else {
+                if (!is_array($persistedCollectionConfig)) {
+                    $persistedCollectionConfig = [];
+                }
                 // Merge selected areas with crop tool configuration
                 reset($persistedCollectionConfig);
                 foreach ($tcaConfig as $id => &$cropVariantConfig) {
@@ -91,6 +93,34 @@ class CropVariantCollection
         return $cropVariantsAsArray;
     }
 
+    /**
+     * @param FileInterface $file
+     * @return CropVariantCollection
+     */
+    public function applyRatioRestrictionToSelectedCropArea(FileInterface $file): CropVariantCollection
+    {
+        $newCollection = clone $this;
+        foreach ($this->cropVariants as $id => $cropVariant) {
+            $newCollection->cropVariants[$id] = $cropVariant->applyRatioRestrictionToSelectedCropArea($file);
+        }
+        return $newCollection;
+    }
+
+    public function __toString()
+    {
+        $filterNonPersistentKeys = function ($key) {
+            if (in_array($key, ['id', 'title', 'allowedAspectRatios', 'coverAreas'], true)) {
+                return false;
+            }
+            return true;
+        };
+        $cropVariantsAsArray = [];
+        foreach ($this->cropVariants as $id => $cropVariant) {
+            $cropVariantsAsArray[$id] = array_filter($cropVariant->asArray(), $filterNonPersistentKeys, ARRAY_FILTER_USE_KEY);
+        }
+        return json_encode($cropVariantsAsArray);
+    }
+
     /**
      * @param string $id
      * @return Area
diff --git a/typo3/sysext/core/Classes/Imaging/ImageManipulation/Ratio.php b/typo3/sysext/core/Classes/Imaging/ImageManipulation/Ratio.php
index e88f56a99baa279dcee80ea689283b62c0fff9f0..d13b41b8eaf8bad5ef1b734c97321516afd02f17 100644
--- a/typo3/sysext/core/Classes/Imaging/ImageManipulation/Ratio.php
+++ b/typo3/sysext/core/Classes/Imaging/ImageManipulation/Ratio.php
@@ -79,4 +79,20 @@ class Ratio
             'value' => $this->value,
         ];
     }
+
+    /**
+     * @return float
+     */
+    public function getRatioValue(): float
+    {
+        return $this->value;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isFree(): bool
+    {
+        return $this->value === 0.0;
+    }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Imaging/ImageManipulation/AreaTest.php b/typo3/sysext/core/Tests/Unit/Imaging/ImageManipulation/AreaTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b3c0be4110b015090c85adb4ecb8ab5d96b27123
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Imaging/ImageManipulation/AreaTest.php
@@ -0,0 +1,93 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Core\Tests\Unit\Imaging\ImageManipulation;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
+use TYPO3\CMS\Core\Imaging\ImageManipulation\Ratio;
+use TYPO3\CMS\Core\Resource\File;
+use TYPO3\CMS\Core\Resource\ResourceStorage;
+use TYPO3\Components\TestingFramework\Core\Unit\UnitTestCase;
+
+class AreaTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function makeRelativeToFileReducesSizes()
+    {
+        $imageArea = new Area(50.0, 50.0, 100.0, 100.0);
+        $imageFixture = new File(
+            [],
+            $this->getMockBuilder(ResourceStorage::class)->disableOriginalConstructor()->getMock(),
+            ['width' => 100, 'height' => 200]
+        );
+        $relativeArea = $imageArea->makeRelativeBasedOnFile($imageFixture);
+        $expectedResult = [
+            'x' => 0.5,
+            'y' => 0.25,
+            'width' => 1.0,
+            'height' => 0.5,
+        ];
+        $this->assertSame($expectedResult, $relativeArea->asArray());
+    }
+
+    public function applyRatioRestrictsAreaToRespectRatioDataProvider()
+    {
+        return [
+            [
+                [0.0, 0.0, 1, 1],
+                4 / 3
+            ],
+            [
+                [0.0, 0.0, 1, 1],
+                3 / 4
+            ],
+            [
+                [0.1, 0.1, 0.2, 0.4],
+                4 / 3,
+            ],
+            [
+                [0.1, 0.1, 0.4, 0.2],
+                1.0
+            ],
+        ];
+    }
+
+    /**
+     * @param array $areaSize
+     * @param $ratio
+     * @test
+     * @dataProvider applyRatioRestrictsAreaToRespectRatioDataProvider
+     */
+    public function applyRatioRestrictsAreaToRespectRatio(array $areaSize, $ratio)
+    {
+        $area = new Area(...$areaSize);
+        $ratioFixture = new Ratio('dummy', 'dummy', $ratio);
+        $areaData = $area->applyRatioRestriction($ratioFixture)->asArray();
+        $this->assertSame($areaData['width'] / $areaData['height'], $ratio);
+    }
+
+    /**
+     * @test
+     */
+    public function applyRatioDoesNothingForFreeRatio()
+    {
+        $area = new Area(...[0.1, 0.1, 0.2, 0.4]);
+        $ratioFixture = new Ratio('dummy', 'dummy', 0.0);
+        $croppedArea = $area->applyRatioRestriction($ratioFixture);
+        $this->assertSame($area, $croppedArea);
+    }
+}