Index: src/js/_enqueues/admin/media.js
===================================================================
--- src/js/_enqueues/admin/media.js	(revision 43335)
+++ src/js/_enqueues/admin/media.js	(working copy)
@@ -204,4 +204,40 @@
 			$( this ).find( '.found-radio input' ).prop( 'checked', true );
 		});
 	});
+
+	/**
+	 * Handle request to create missing image sizes.
+	 */
+	$( document ).ready( function() {
+		$( document ).on( 'click.wp-update-image-sizes', function( event ) {
+			const $target = $( event.target );
+
+			if ( $target.is( 'button.wp-update-image-sizes' ) ) {
+				var $parent = $target.parent();
+				var $message = $parent.find( 'span.message' );
+				var $spinner = $parent.find( 'span.spinner' );
+
+				$spinner.addClass( 'is-active' );
+
+				$.ajax({
+					type: 'post',
+					url: window.ajaxurl,
+					dataType: 'json',
+					data: {
+						action: 'wp-update-image-sizes',
+						update_sizes_nonce: $target.attr( 'data-wp-nonce' ),
+						attachment_id: $target.attr( 'data-wp-attachment-id' ),
+					}
+				}).always( function() {
+					$spinner.removeClass( 'is-active' );
+				}).done( function( response ) {
+					if ( response.data && response.data.message ) {
+						$message.text( response.data.message );
+					}
+				}).fail( function() {
+					$message.text( attachMediaBoxL10n.error );
+				});
+			}
+		} );
+	} );
 })( jQuery );
Index: src/wp-admin/admin-ajax.php
===================================================================
--- src/wp-admin/admin-ajax.php	(revision 43335)
+++ src/wp-admin/admin-ajax.php	(working copy)
@@ -131,6 +131,7 @@
 	'edit-theme-plugin-file',
 	'wp-privacy-export-personal-data',
 	'wp-privacy-erase-personal-data',
+	'wp-update-image-sizes',
 );
 
 // Deprecated
Index: src/wp-admin/css/list-tables.css
===================================================================
--- src/wp-admin/css/list-tables.css	(revision 43335)
+++ src/wp-admin/css/list-tables.css	(working copy)
@@ -360,7 +360,8 @@
 	vertical-align: top; /* Remove descender white-space. */
 }
 
-table.media .column-title .has-media-icon ~ .row-actions {
+table.media .column-title .has-media-icon ~ .row-actions,
+table.media .column-title .has-media-icon ~ .update-image-sizes {
 	margin-left: 70px; /* 60px image + margin */
 }
 
@@ -368,6 +369,11 @@
 	margin-bottom: 0.2em;
 }
 
+table.media .update-image-sizes .spinner {
+	float: none;
+	margin: 2px 10px;
+}
+
 /* @todo: pick a consistent list table selector */
 .wp-list-table a {
 	transition: none;
Index: src/wp-admin/includes/ajax-actions.php
===================================================================
--- src/wp-admin/includes/ajax-actions.php	(revision 43335)
+++ src/wp-admin/includes/ajax-actions.php	(working copy)
@@ -4726,3 +4726,31 @@
 
 	wp_send_json_success( $response );
 }
+
+/**
+ * Ajax handler for updating image sizes.
+ *
+ * @since 5.0.0
+ */
+function wp_ajax_wp_update_image_sizes() {
+	if ( empty( $_POST['attachment_id'] ) ) {
+		wp_send_json_error( __( 'Missing attachment ID.' ) );
+	}
+
+	$attachment_id = (int) $_POST['attachment_id'];
+
+	if ( ! current_user_can( 'edit_post', $attachment_id ) || ! wp_verify_nonce( $_POST['update_sizes_nonce'], 'update_image_sizes' . $attachment_id ) ) {
+		wp_send_json_error( __( 'Invalid request.' ) );
+	}
+
+	$result = wp_update_image_attachment_sizes( $attachment_id );
+
+	if ( false === $result ) {
+		$response = array( 'message' => __( 'New image sub-sizes were not created. All sub-sizes already exist.' ) );
+	} else {
+		$count = count( $result );
+		$response = array( 'message' => sprintf( _n( 'Created %d image sub-size.', 'Created %d image sub-sizes.', $count ), $count ) );
+	}
+
+	wp_send_json_success( $response );
+}
Index: src/wp-admin/includes/class-wp-media-list-table.php
===================================================================
--- src/wp-admin/includes/class-wp-media-list-table.php	(revision 43335)
+++ src/wp-admin/includes/class-wp-media-list-table.php	(working copy)
@@ -411,6 +411,24 @@
 			?>
 		</p>
 		<?php
+		$missing_sizes = wp_check_image_attachment_sizes( $post->ID );
+
+		if ( ! empty( $missing_sizes ) ) {
+			$nonce = wp_create_nonce( 'update_image_sizes' . $post->ID );
+			$count = count( $missing_sizes );
+
+			?>
+			<p class="update-image-sizes">
+				<span class="message">
+					<?php printf( _n( '%s image sub-size file is missing.', '%s image sub-size files are missing.', $count ), $count ); ?>
+				</span>
+				<button type="button" class="button button-small wp-update-image-sizes" data-wp-nonce="<?php echo $nonce; ?>" data-wp-attachment-id="<?php echo $post->ID; ?>">
+				<?php _e( 'Refresh image sizes' ); ?>
+				</button>
+				<span class="spinner"></span>
+			</p>
+			<?php
+		}
 	}
 
 	/**
Index: src/wp-admin/includes/image.php
===================================================================
--- src/wp-admin/includes/image.php	(revision 43335)
+++ src/wp-admin/includes/image.php	(working copy)
@@ -66,9 +66,200 @@
 	return $dst_file;
 }
 
+function wp_check_image_attachment_sizes( $attachment_id ) {
+	if ( ! wp_attachment_is_image( $attachment_id ) ) {
+		return false;
+	}
+
+	$image_meta = wp_get_attachment_metadata( $attachment_id );
+	$defined_sizes = _wp_get_intermediate_image_sizes_with_data();
+
+	// Skip sizes that are too large.
+	foreach ( $defined_sizes as $size_name => $size_data ) {
+		$width = ! empty( $size_data['width'] ) ? (int) $size_data['width'] : 0;
+		$height = ! empty( $size_data['height'] ) ? (int) $size_data['height'] : 0;
+
+		if ( $width && $height ) {
+			if ( $width > $image_meta['width'] && $height > $image_meta['height'] ) {
+				unset( $defined_sizes[ $size_name ] );
+			}
+		} elseif ( ! $width ) {
+			if ( $height > $image_meta['height'] ) {
+				unset( $defined_sizes[ $size_name ] );
+			}
+		} elseif ( ! $height ) {
+			if ( $width > $image_meta['width'] ) {
+				unset( $defined_sizes[ $size_name ] );
+			}
+		}
+	}
+
+	if ( empty( $image_meta['sizes'] ) ) {
+		$image_meta['sizes'] = array();
+	}
+
+	$missing_sizes = array_diff( array_keys( $defined_sizes ), array_keys( $image_meta['sizes'] ) );
+
+	/**
+	 * Filters the missing image size files per attachment.
+	 *
+	 * @since 5.0.0
+	 *
+	 * @param array $missing_sizes Array with the missing image sizes.
+	 * @param array $image_meta The image attachment meta data.
+	 * @param string $attachment_id The attachment post ID.
+	 */
+	return apply_filters( 'wp_check_image_attachment_sizes', $missing_sizes, $image_meta, $attachment_id );
+}
+
 /**
- * Generate post thumbnail attachment meta data.
+ * For an image attachment, if any of the currently defined image sub-sizes are missing,
+ * creates them and updates the image meta data.
  *
+ * @since 5.0
+ *
+ * @param int $attachment_id The image attachment post ID.
+ * @return bool Whether any sizes were added.
+ */
+function wp_update_image_attachment_sizes( $attachment_id ) {
+	$missing_sizes = wp_check_image_attachment_sizes( $attachment_id );
+
+	if ( ! empty( $missing_sizes ) ) {
+		$file = get_attached_file( $attachment_id );
+		$image_meta = wp_get_attachment_metadata( $attachment_id );
+		$new_meta = _wp_create_image_subsizes( $file, $image_meta, $attachment_id );
+
+		$updated_sizes = array_diff_assoc( $new_meta['sizes'], $image_meta['sizes'] );
+
+		if ( ! empty( $updated_sizes ) ) {
+			return $updated_sizes;
+		}
+	}
+
+	return false;
+}
+
+function _wp_get_intermediate_image_sizes_with_data() {
+	$_wp_additional_image_sizes = wp_get_additional_image_sizes();
+	$sizes = array();
+
+	foreach ( get_intermediate_image_sizes() as $size ) {
+		$size_data = array(
+			'width'  => '',
+			'height' => '',
+			'crop'   => false,
+		);
+
+		if ( isset( $_wp_additional_image_sizes[ $size ]['width'] ) ) {
+			// For theme-added sizes
+			$size_data['width'] = intval( $_wp_additional_image_sizes[ $size ]['width'] );
+		} else {
+			// For default sizes set in options
+			$size_data['width'] = get_option( "{$size}_size_w" );
+		}
+
+		if ( isset( $_wp_additional_image_sizes[ $size ]['height'] ) ) {
+			// For theme-added sizes
+			$size_data['height'] = intval( $_wp_additional_image_sizes[ $size ]['height'] );
+		} else {
+			// For default sizes set in options
+			$size_data['height'] = get_option( "{$size}_size_h" );
+		}
+
+		if ( isset( $_wp_additional_image_sizes[ $size ]['crop'] ) ) {
+			// For theme-added sizes
+			$size_data['crop'] = $_wp_additional_image_sizes[ $size ]['crop'];
+		} else {
+			// For default sizes set in options
+			$size_data['crop'] = get_option( "{$size}_crop" );
+		}
+
+		if ( empty( $size_data['width'] ) && empty( $size_data['height'] ) ) {
+			continue;
+		}
+
+		$sizes[ $size ] = $size_data;
+	}
+
+	return $sizes;
+}
+
+/**
+ * Create image sub-sizes and add the related data to the image meta `sizes` array.
+ *
+ * Saves/updates the image meta after each sub-size is created.
+ *
+ * @since 5.0
+ *
+ * @param string $file Full filepath of the attached image.
+ * @param array $image_meta The attachment meta data.
+ * @param int $attachment_id Attachment Id to process.
+ * @return array The attachment meta data with updated `sizes` array.
+ */
+function _wp_create_image_subsizes( $file, $image_meta, $attachment_id ) {
+	$new_sizes = _wp_get_intermediate_image_sizes_with_data();
+	/**
+	 * Filters the image sizes automatically generated when uploading an image.
+	 *
+	 * @since 2.9.0
+	 * @since 4.4.0 Added the `$metadata` argument.
+	 * @since 5.0.0 Added the `$attachment_id` argument.
+	 *
+	 * @param array $sizes         An associative array of image sizes.
+	 * @param array $metadata      An associative array of image metadata: width, height, file.
+	 * @param int   $attachment_id Current attachment ID.
+	 */
+	$new_sizes = apply_filters( 'intermediate_image_sizes_advanced', $new_sizes, $image_meta, $attachment_id );
+
+	// Check if any of the sizes already exist.
+	if ( ! empty( $image_meta['sizes'] ) && is_array( $image_meta['sizes'] ) ) {
+		foreach ( $image_meta['sizes'] as $size_name => $size_data ) {
+			if ( array_key_exists( $size_name, $new_sizes ) ) {
+				// Can check here if file exists..?
+				unset( $new_sizes[ $size_name ] );
+			} else {
+				// Make sure we are not adding the exact same file under different "size name".
+				foreach ( $new_sizes as $new_size_name => $new_size_data ) {
+					if ( $size_data['width'] === $new_size_data['width'] || $size_data['height'] === $new_size_data['height'] ) {
+						// $new_size_data['width'] or $new_size_data['height'] may be empty (meaning the new image size has either max-width or max-height),
+						// or they may be set to max-width and max-height like the default "large" size (1024px width and 1024px height).
+						$check = wp_constrain_dimensions( $size_data['width'], $size_data['height'], $new_size_data['width'], $new_size_data['height'] );
+
+						if ( $check[0] === $size_data['width'] && $check[1] === $size_data['height'] ) {
+							unset( $new_sizes[ $new_size_name ] );
+						}
+					}
+				}
+			}
+		}
+	}
+
+	if ( ! empty( $new_sizes ) ) {
+		$editor = wp_get_image_editor( $file );
+
+		if ( ! is_wp_error( $editor ) ) {
+			if ( empty( $image_meta['sizes'] ) ) {
+				$image_meta['sizes'] = array();
+			}
+
+			foreach ( $new_sizes as $new_size_name => $new_size_data ) {
+				$size_meta = $editor->make_subsize( $new_size_data );
+
+				if ( ! is_wp_error( $size_meta ) ) {
+					// The sub-size was created successfully. Save the size meta value.
+					$image_meta['sizes'][ $new_size_name ] = $size_meta;
+					wp_update_attachment_metadata( $attachment_id, $image_meta );
+				}
+			}
+		}
+	}
+
+	return $image_meta;
+}
+
+/**
+ * Generate attachment meta data and create image sub-sizes for images.
+ *
  * @since 2.1.0
  *
  * @param int $attachment_id Attachment Id to process.
@@ -90,69 +281,14 @@
 		// Make the file path relative to the upload dir.
 		$metadata['file'] = _wp_relative_upload_path( $file );
 
-		// Make thumbnails and other intermediate sizes.
-		$_wp_additional_image_sizes = wp_get_additional_image_sizes();
-
-		$sizes = array();
-		foreach ( get_intermediate_image_sizes() as $s ) {
-			$sizes[ $s ] = array(
-				'width'  => '',
-				'height' => '',
-				'crop'   => false,
-			);
-			if ( isset( $_wp_additional_image_sizes[ $s ]['width'] ) ) {
-				// For theme-added sizes
-				$sizes[ $s ]['width'] = intval( $_wp_additional_image_sizes[ $s ]['width'] );
-			} else {
-				// For default sizes set in options
-				$sizes[ $s ]['width'] = get_option( "{$s}_size_w" );
-			}
-
-			if ( isset( $_wp_additional_image_sizes[ $s ]['height'] ) ) {
-				// For theme-added sizes
-				$sizes[ $s ]['height'] = intval( $_wp_additional_image_sizes[ $s ]['height'] );
-			} else {
-				// For default sizes set in options
-				$sizes[ $s ]['height'] = get_option( "{$s}_size_h" );
-			}
-
-			if ( isset( $_wp_additional_image_sizes[ $s ]['crop'] ) ) {
-				// For theme-added sizes
-				$sizes[ $s ]['crop'] = $_wp_additional_image_sizes[ $s ]['crop'];
-			} else {
-				// For default sizes set in options
-				$sizes[ $s ]['crop'] = get_option( "{$s}_crop" );
-			}
-		}
-
-		/**
-		 * Filters the image sizes automatically generated when uploading an image.
-		 *
-		 * @since 2.9.0
-		 * @since 4.4.0 Added the `$metadata` argument.
-		 * @since 5.0.0 Added the `$attachment_id` argument.
-		 *
-		 * @param array $sizes         An associative array of image sizes.
-		 * @param array $metadata      An associative array of image metadata: width, height, file.
-		 * @param int   $attachment_id Current attachment ID.
-		 */
-		$sizes = apply_filters( 'intermediate_image_sizes_advanced', $sizes, $metadata, $attachment_id );
-
-		if ( $sizes ) {
-			$editor = wp_get_image_editor( $file );
-
-			if ( ! is_wp_error( $editor ) ) {
-				$metadata['sizes'] = $editor->multi_resize( $sizes );
-			}
-		} else {
-			$metadata['sizes'] = array();
-		}
-
 		// Fetch additional metadata from EXIF/IPTC.
 		$image_meta = wp_read_image_metadata( $file );
 		if ( $image_meta ) {
 			$metadata['image_meta'] = $image_meta;
 		}
+
+		// Make thumbnails and other intermediate sizes.
+		$metadata = _wp_create_image_subsizes( $file, $metadata, $attachment_id );
 	} elseif ( wp_attachment_is( 'video', $attachment ) ) {
 		$metadata = wp_read_video_metadata( $file );
 		$support  = current_theme_supports( 'post-thumbnails', 'attachment:video' ) || post_type_supports( 'attachment:video', 'thumbnail' );
@@ -437,7 +573,7 @@
 		}
 	}
 
-	$exif = array(); 
+	$exif = array();
 
 	/**
 	 * Filters the image types to check for exif data.
Index: src/wp-includes/class-wp-image-editor-gd.php
===================================================================
--- src/wp-includes/class-wp-image-editor-gd.php	(revision 43335)
+++ src/wp-includes/class-wp-image-editor-gd.php	(working copy)
@@ -217,43 +217,75 @@
 	 * @return array An array of resized images' metadata by size.
 	 */
 	public function multi_resize( $sizes ) {
-		$metadata  = array();
-		$orig_size = $this->size;
+		$metadata = array();
 
 		foreach ( $sizes as $size => $size_data ) {
-			if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
-				continue;
-			}
+			$meta = $this->make_subsize( $size_data );
 
-			if ( ! isset( $size_data['width'] ) ) {
-				$size_data['width'] = null;
+			if ( ! is_wp_error( $meta ) ) {
+				$metadata[ $size ] = $meta;
 			}
-			if ( ! isset( $size_data['height'] ) ) {
-				$size_data['height'] = null;
-			}
+		}
 
-			if ( ! isset( $size_data['crop'] ) ) {
-				$size_data['crop'] = false;
-			}
+		return $metadata;
+	}
 
-			$image     = $this->_resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
-			$duplicate = ( ( $orig_size['width'] == $size_data['width'] ) && ( $orig_size['height'] == $size_data['height'] ) );
+	/**
+	 * Create an image sub-size and return the image meta data value for it.
+	 *
+	 * @since 5.0
+	 *
+	 * @param array $size_data Array of height, width and whether to crop.
+	 * @return WP_Error|array WP_Error on error, or the image data array for inclusion in the `sizes` array in the image meta.
+	 */
+	public function make_subsize( $size_data ) {
+		if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
+			return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) );
+		}
 
-			if ( ! is_wp_error( $image ) && ! $duplicate ) {
-				$resized = $this->_save( $image );
+		$orig_size  = $this->size;
+		$resized    = false;
 
-				imagedestroy( $image );
+		if ( ! isset( $size_data['width'] ) ) {
+			$size_data['width'] = null;
+		}
 
-				if ( ! is_wp_error( $resized ) && $resized ) {
-					unset( $resized['path'] );
-					$metadata[ $size ] = $resized;
-				}
+		if ( ! isset( $size_data['height'] ) ) {
+			$size_data['height'] = null;
+		}
+
+		if ( ! isset( $size_data['crop'] ) ) {
+			$size_data['crop'] = false;
+		}
+
+		$image     = $this->_resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
+		$duplicate = ( ( $orig_size['width'] == $size_data['width'] ) && ( $orig_size['height'] == $size_data['height'] ) );
+
+		if ( is_wp_error( $image ) ) {
+			return $image;
+		}
+
+		if ( ! $duplicate ) {
+			$resized = $this->_save( $image );
+
+			imagedestroy( $image );
+
+			if ( is_wp_error( $resized ) ) {
+				return $resized;
 			}
 
-			$this->size = $orig_size;
+			if ( $resized ) {
+				unset( $resized['path'] );
+			}
 		}
 
-		return $metadata;
+		$this->size = $orig_size;
+
+		if ( ! $resized ) {
+			return new WP_Error( 'image_subsize_create_error', __( 'Failed to create image sub-size.' ) );
+		}
+
+		return $resized;
 	}
 
 	/**
Index: src/wp-includes/class-wp-image-editor-imagick.php
===================================================================
--- src/wp-includes/class-wp-image-editor-imagick.php	(revision 43335)
+++ src/wp-includes/class-wp-image-editor-imagick.php	(working copy)
@@ -413,7 +413,7 @@
 	 * @since 3.5.0
 	 *
 	 * @param array $sizes {
-	 *     An array of image size arrays. Default sizes are 'small', 'medium', 'medium_large', 'large'.
+	 *     An array of image size arrays. Default sizes are 'thumbnail', 'medium', 'medium_large', 'large'.
 	 *
 	 *     Either a height or width must be provided.
 	 *     If one of the two is set to null, the resize will
@@ -430,52 +430,79 @@
 	 * @return array An array of resized images' metadata by size.
 	 */
 	public function multi_resize( $sizes ) {
-		$metadata   = array();
-		$orig_size  = $this->size;
-		$orig_image = $this->image->getImage();
+		$metadata = array();
 
 		foreach ( $sizes as $size => $size_data ) {
-			if ( ! $this->image ) {
-				$this->image = $orig_image->getImage();
-			}
+			$meta = $this->make_subsize( $size_data );
 
-			if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
-				continue;
+			if ( ! is_wp_error( $meta ) ) {
+				$metadata[ $size ] = $meta;
 			}
+		}
 
-			if ( ! isset( $size_data['width'] ) ) {
-				$size_data['width'] = null;
-			}
-			if ( ! isset( $size_data['height'] ) ) {
-				$size_data['height'] = null;
-			}
+		return $metadata;
+	}
 
-			if ( ! isset( $size_data['crop'] ) ) {
-				$size_data['crop'] = false;
-			}
+	/**
+	 * Create an image sub-size and return the image meta data value for it.
+	 *
+	 * @since 5.0.0
+	 *
+	 * @param array $size_data Array of height, width and whether to crop.
+	 * @return WP_Error|array WP_Error on error, or the image data array for inclusion in the `sizes` array in the image meta.
+	 */
+	public function make_subsize( $size_data ) {
+		if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
+			return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) );
+		}
 
-			$resize_result = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
-			$duplicate     = ( ( $orig_size['width'] == $size_data['width'] ) && ( $orig_size['height'] == $size_data['height'] ) );
+		$orig_size  = $this->size;
+		$orig_image = $this->image->getImage();
+		$resized    = false;
 
-			if ( ! is_wp_error( $resize_result ) && ! $duplicate ) {
-				$resized = $this->_save( $this->image );
+		if ( ! isset( $size_data['width'] ) ) {
+			$size_data['width'] = null;
+		}
 
-				$this->image->clear();
-				$this->image->destroy();
-				$this->image = null;
+		if ( ! isset( $size_data['height'] ) ) {
+			$size_data['height'] = null;
+		}
 
-				if ( ! is_wp_error( $resized ) && $resized ) {
-					unset( $resized['path'] );
-					$metadata[ $size ] = $resized;
-				}
+		if ( ! isset( $size_data['crop'] ) ) {
+			$size_data['crop'] = false;
+		}
+
+		$resize_result = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
+		$duplicate     = ( ( $orig_size['width'] == $size_data['width'] ) && ( $orig_size['height'] == $size_data['height'] ) );
+
+		if ( is_wp_error( $resize_result ) ) {
+			return $resize_result;
+		}
+
+		if ( ! $duplicate ) {
+			$resized = $this->_save( $this->image );
+
+			$this->image->clear();
+			$this->image->destroy();
+			$this->image = null;
+
+			if ( is_wp_error( $resized ) ) {
+				return $resized;
 			}
 
-			$this->size = $orig_size;
+			if ( $resized ) {
+				unset( $resized['path'] );
+			}
 		}
 
-		$this->image = $orig_image;
+		$this->size = $orig_size;
+		$this->image = $orig_image->getImage();
 
-		return $metadata;
+		if ( ! $resized ) {
+			return new WP_Error( 'image_subsize_create_error', __( 'Failed to create image sub-size.' ) );
+		}
+
+		return $resized;
 	}
 
 	/**
