From 5886225b1c348645ded71f24a0a5765115917025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Mon, 22 Jun 2020 00:16:31 +0300 Subject: [PATCH] Image editing: batch editing in cropper component (#23284) * Image editing: batch editing in cropper component * For rotation, set new image url because rotation is limited in cropper * Don' add editor for images not hosted on the server * Fix loading indicator * Fix image edit without rotate * Reorg buttons * Recalculate height after aspect ratio change * Remove unused endpoints * Fix php lint errors --- lib/class-wp-rest-image-editor-controller.php | 195 +++++----- lib/image-editor/class-image-editor-crop.php | 126 ------ lib/image-editor/class-image-editor-flip.php | 127 ------- .../class-image-editor-rotate.php | 104 ----- lib/image-editor/class-image-editor.php | 295 --------------- packages/block-library/src/image/edit.js | 15 +- packages/block-library/src/image/editor.scss | 39 +- .../block-library/src/image/image-editor.js | 358 +++++++----------- packages/block-library/src/image/image.js | 78 ++-- 9 files changed, 306 insertions(+), 1031 deletions(-) delete mode 100644 lib/image-editor/class-image-editor-crop.php delete mode 100644 lib/image-editor/class-image-editor-flip.php delete mode 100644 lib/image-editor/class-image-editor-rotate.php delete mode 100644 lib/image-editor/class-image-editor.php diff --git a/lib/class-wp-rest-image-editor-controller.php b/lib/class-wp-rest-image-editor-controller.php index 8be0dd425834e..e572f665bb5c8 100644 --- a/lib/class-wp-rest-image-editor-controller.php +++ b/lib/class-wp-rest-image-editor-controller.php @@ -7,11 +7,6 @@ * @subpackage REST_API */ -/** - * Image editor - */ -include_once __DIR__ . '/image-editor/class-image-editor.php'; - /** * Controller which provides REST API endpoints for image editing. * @@ -30,7 +25,6 @@ class WP_REST_Image_Editor_Controller extends WP_REST_Controller { public function __construct() { $this->namespace = '__experimental'; $this->rest_base = '/richimage/(?P[\d]+)'; - $this->editor = new Image_Editor(); } /** @@ -42,75 +36,35 @@ public function __construct() { public function register_routes() { register_rest_route( $this->namespace, - $this->rest_base . '/rotate', - array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'rotate_image' ), - 'permission_callback' => array( $this, 'permission_callback' ), - 'args' => array( - 'angle' => array( - 'description' => __( 'Rotation angle', 'gutenberg' ), - 'type' => 'integer', - 'required' => true, - ), - ), - ), - ) - ); - - register_rest_route( - $this->namespace, - $this->rest_base . '/flip', + $this->rest_base . '/apply', array( array( 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'flip_image' ), + 'callback' => array( $this, 'apply_edits' ), 'permission_callback' => array( $this, 'permission_callback' ), 'args' => array( - 'direction' => array( - 'description' => __( 'Flip direction', 'gutenberg' ), - 'type' => 'string', - 'enum' => array( 'vertical', 'horizontal' ), - 'required' => true, + 'x' => array( + 'type' => 'float', + 'minimum' => 0, + 'required' => true, ), - ), - ), - ) - ); - - register_rest_route( - $this->namespace, - $this->rest_base . '/crop', - array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'crop_image' ), - 'permission_callback' => array( $this, 'permission_callback' ), - 'args' => array( - 'crop_x' => array( - 'description' => __( 'Crop offset percentage from left', 'gutenberg' ), - 'type' => 'number', - 'minimum' => 0, - 'required' => true, + 'y' => array( + 'type' => 'float', + 'minimum' => 0, + 'required' => true, ), - 'crop_y' => array( - 'description' => __( 'Crop offset percentage from top', 'gutenberg' ), - 'type' => 'number', - 'minimum' => 0, - 'required' => true, + 'width' => array( + 'type' => 'float', + 'minimum' => 1, + 'required' => true, ), - 'crop_width' => array( - 'description' => __( 'Crop width percentage', 'gutenberg' ), - 'type' => 'number', - 'minimum' => 1, - 'required' => true, + 'height' => array( + 'type' => 'float', + 'minimum' => 1, + 'required' => true, ), - 'crop_height' => array( - 'description' => __( 'Crop height percentage', 'gutenberg' ), - 'type' => 'number', - 'minimum' => 1, - 'required' => true, + 'rotation' => array( + 'type' => 'integer', ), ), ), @@ -136,7 +90,7 @@ public function permission_callback( $request ) { } /** - * Rotates an image. + * Applies all edits in one go. * * @since 7.x ? * @access public @@ -144,39 +98,88 @@ public function permission_callback( $request ) { * @param WP_REST_Request $request Full details about the request. * @return array|WP_Error If successful image JSON for the modified image, otherwise a WP_Error. */ - public function rotate_image( $request ) { - $modifier = new Image_Editor_Rotate( $request['angle'] ); + public function apply_edits( $request ) { + require_once ABSPATH . 'wp-admin/includes/image.php'; - return $this->editor->modify_image( $request['media_id'], $modifier ); - } + $params = $request->get_params(); - /** - * Flips/mirrors an image. - * - * @since 7.x ? - * @access public - * - * @param WP_REST_Request $request Full details about the request. - * @return array|WP_Error If successful image JSON for the modified image, otherwise a WP_Error. - */ - public function flip_image( $request ) { - $modifier = new Image_Editor_Flip( $request['direction'] ); + $media_id = $params['media_id']; - return $this->editor->modify_image( $request['media_id'], $modifier ); - } + // Get image information. + $attachment_info = wp_get_attachment_metadata( $media_id ); + $media_url = wp_get_attachment_image_url( $media_id, 'original' ); - /** - * Crops an image. - * - * @since 7.x ? - * @access public - * - * @param WP_REST_Request $request Full details about the request. - * @return array|WP_Error If successful image JSON for the modified image, otherwise a WP_Error. - */ - public function crop_image( $request ) { - $modifier = new Image_Editor_Crop( $request['crop_x'], $request['crop_y'], $request['crop_width'], $request['crop_height'] ); + if ( ! $attachment_info || ! $media_url ) { + return new WP_Error( 'unknown', 'Unable to get meta information for file' ); + } + + $meta = array( 'original_name' => basename( $media_url ) ); + + if ( isset( $attachment_info['richimage'] ) ) { + $meta = array_merge( $meta, $attachment_info['richimage'] ); + } + + // Try and load the image itself. + $image_path = get_attached_file( $media_id ); + if ( empty( $image_path ) ) { + return new WP_Error( 'fileunknown', 'Unable to find original media file' ); + } + + $image_editor = wp_get_image_editor( $image_path ); + if ( ! $image_editor->load() ) { + return new WP_Error( 'fileload', 'Unable to load original media file' ); + } - return $this->editor->modify_image( $request['media_id'], $modifier ); + $size = $image_editor->get_size(); + + // Finally apply the modifications. + $crop_x = round( ( $size['width'] * floatval( $params['x'] ) ) / 100.0 ); + $crop_y = round( ( $size['height'] * floatval( $params['y'] ) ) / 100.0 ); + $width = round( ( $size['width'] * floatval( $params['width'] ) ) / 100.0 ); + $height = round( ( $size['height'] * floatval( $params['height'] ) ) / 100.0 ); + $image_editor->crop( $crop_x, $crop_y, $width, $height ); + + if ( isset( $params['rotation'] ) ) { + $image_editor->rotate( 0 - $params['rotation'] ); + } + + // TODO: Generate filename based on edits. + $target_file = 'edited-' . $meta['original_name']; + + $filename = rtrim( dirname( $image_path ), '/' ) . '/' . $target_file; + + // Save to disk. + $saved = $image_editor->save( $filename ); + + if ( is_wp_error( $saved ) ) { + return $saved; + } + + // Update attachment details. + $attachment_post = array( + 'guid' => $saved['path'], + 'post_mime_type' => $saved['mime-type'], + 'post_title' => pathinfo( $target_file, PATHINFO_FILENAME ), + 'post_content' => '', + 'post_status' => 'inherit', + ); + + // Add this as an attachment. + $attachment_id = wp_insert_attachment( $attachment_post, $saved['path'], 0 ); + if ( 0 === $attachment_id ) { + return new WP_Error( 'attachment', 'Unable to add image as attachment' ); + } + + // Generate thumbnails. + $metadata = wp_generate_attachment_metadata( $attachment_id, $saved['path'] ); + + $metadata['richimage'] = $meta; + + wp_update_attachment_metadata( $attachment_id, $metadata ); + + return array( + 'media_id' => $attachment_id, + 'url' => wp_get_attachment_image_url( $attachment_id, 'original' ), + ); } } diff --git a/lib/image-editor/class-image-editor-crop.php b/lib/image-editor/class-image-editor-crop.php deleted file mode 100644 index 422d0994bfaad..0000000000000 --- a/lib/image-editor/class-image-editor-crop.php +++ /dev/null @@ -1,126 +0,0 @@ -crop_x = floatval( $crop_x ); - $this->crop_y = floatval( $crop_y ); - $this->width = floatval( $width ); - $this->height = floatval( $height ); - } - - /** - * Update the image metadata with the modifier. - * - * @access public - * - * @param array $meta Metadata to update. - * @return array Updated metadata. - */ - public function apply_to_meta( $meta ) { - $meta['crop_x'] = $this->crop_x; - $meta['crop_y'] = $this->crop_y; - $meta['crop_width'] = $this->width; - $meta['crop_height'] = $this->height; - - return $meta; - } - - /** - * Apply the modifier to the image - * - * @access public - * - * @param WP_Image_Editor $image Image editor. - * @return bool|WP_Error True on success, WP_Error object or false on failure. - */ - public function apply_to_image( $image ) { - $size = $image->get_size(); - - $crop_x = round( ( $size['width'] * $this->crop_x ) / 100.0 ); - $crop_y = round( ( $size['height'] * $this->crop_y ) / 100.0 ); - $width = round( ( $size['width'] * $this->width ) / 100.0 ); - $height = round( ( $size['height'] * $this->height ) / 100.0 ); - - return $image->crop( $crop_x, $crop_y, $width, $height ); - } - - /** - * Gets the new filename based on metadata. - * - * @access public - * - * @param array $meta Image metadata. - * @return string Filename for the edited image. - */ - public static function get_filename( $meta ) { - if ( isset( $meta['crop_width'] ) && $meta['crop_width'] > 0 ) { - $target_file = sprintf( 'crop-%d-%d-%d-%d', round( $meta['crop_x'], 2 ), round( $meta['crop_y'], 2 ), round( $meta['crop_width'], 2 ), round( $meta['crop_height'], 2 ) ); - - // We need to change the original name to include the crop. This way if it's cropped again we won't clash. - $meta['original_name'] = $target_file; - - return $target_file; - } - - return false; - } - - /** - * Gets the default metadata for the crop modifier. - * - * @access public - * - * @return array Default metadata. - */ - public static function get_default_meta() { - return array(); - } -} diff --git a/lib/image-editor/class-image-editor-flip.php b/lib/image-editor/class-image-editor-flip.php deleted file mode 100644 index f9b4797c99e94..0000000000000 --- a/lib/image-editor/class-image-editor-flip.php +++ /dev/null @@ -1,127 +0,0 @@ -direction = 'vertical'; - - if ( 'horizontal' === $direction ) { - $this->direction = $direction; - } - } - - /** - * Update the image metadata with the modifier. - * - * @access public - * - * @param array $meta Metadata to update. - * @return array Updated metadata. - */ - public function apply_to_meta( $meta ) { - if ( $this->is_vertical() ) { - $meta['flip_vertical'] = ! $meta['flip_vertical']; - } elseif ( $this->is_horizontal() ) { - $meta['flip_horizontal'] = ! $meta['flip_horizontal']; - } - - return $meta; - } - - /** - * Apply the modifier to the image - * - * @access public - * - * @param WP_Image_Editor $image Image editor. - * @return bool|WP_Error True on success, WP_Error object or false on failure. - */ - public function apply_to_image( $image ) { - return $image->flip( $this->is_vertical(), $this->is_horizontal() ); - } - - /** - * Checks if the modifier is a vertical flip - * - * @access private - * - * @return boolean true if the modifier is vertical - */ - private function is_vertical() { - return 'vertical' === $this->direction; - } - - /** - * Checks if the modifier is a horizontal flip - * - * @access private - * - * @return boolean true if the modifier is horizontal - */ - private function is_horizontal() { - return 'horizontal' === $this->direction; - } - - /** - * Gets the new filename based on metadata. - * - * @access public - * - * @param array $meta Image metadata. - * @return string Filename for the edited image. - */ - public static function get_filename( $meta ) { - $parts = array(); - - if ( $meta['flip_horizontal'] ) { - $parts[] = 'flip_horizontal'; - } - - if ( $meta['flip_vertical'] ) { - $parts[] = 'flip_vertical'; - } - - if ( count( $parts ) > 0 ) { - return implode( '-', $parts ); - } - - return false; - } - - /** - * Gets the default metadata for the flip modifier. - * - * @access public - * - * @return array Default metadata. - */ - public static function get_default_meta() { - return array( - 'flip_horizontal' => false, - 'flip_vertical' => false, - ); - } -} diff --git a/lib/image-editor/class-image-editor-rotate.php b/lib/image-editor/class-image-editor-rotate.php deleted file mode 100644 index 82edd5524e80c..0000000000000 --- a/lib/image-editor/class-image-editor-rotate.php +++ /dev/null @@ -1,104 +0,0 @@ -angle = $this->restrict_angle( intval( $angle, 10 ) ); - } - - /** - * Update the image metadata with the modifier. - * - * @access public - * - * @param array $meta Metadata to update. - * @return array Updated metadata. - */ - public function apply_to_meta( $meta ) { - $meta['rotate'] += $this->angle; - $meta['rotate'] = $this->restrict_angle( $meta['rotate'] ); - - return $meta; - } - - /** - * Apply the rotate modifier to the image - * - * @access public - * - * @param WP_Image_Editor $image Image editor. - * @return bool|WP_Error True on success, WP_Error object or false on failure. - */ - public function apply_to_image( $image ) { - return $image->rotate( 0 - $this->angle ); - } - - /** - * Puts the angle in the range [ 0, 360 ). - * - * @access private - * - * @param integer $angle Angle to restrict. - * @return integer Restricted angle. - */ - private function restrict_angle( $angle ) { - if ( $angle >= 360 ) { - $angle = $angle % 360; - } elseif ( $angle < 0 ) { - $angle = 360 - ( abs( $angle ) % 360 ); - } - - return $angle; - } - - /** - * Gets the new filename based on metadata. - * - * @access public - * - * @param array $meta Image metadata. - * @return string Filename for the edited image. - */ - public static function get_filename( $meta ) { - if ( $meta['rotate'] > 0 ) { - return 'rotate-' . intval( $meta['rotate'], 10 ); - } - - return false; - } - - /** - * Gets the default metadata for the rotate modifier. - * - * @access public - * - * @return array Default metadata. - */ - public static function get_default_meta() { - return array( 'rotate' => 0 ); - } -} diff --git a/lib/image-editor/class-image-editor.php b/lib/image-editor/class-image-editor.php deleted file mode 100644 index a201f65453a37..0000000000000 --- a/lib/image-editor/class-image-editor.php +++ /dev/null @@ -1,295 +0,0 @@ -all_modifiers = array( - 'Image_Editor_Crop', - 'Image_Editor_Flip', - 'Image_Editor_Rotate', - ); - } - - /** - * Modifies an image. - * - * @param integer $media_id Media id. - * @param Image_Editor_Modifier $modifier Modifier to apply to the image. - * @return array|WP_Error If successful image JSON containing the media_id and url of modified image, otherwise WP_Error. - */ - public function modify_image( $media_id, $modifier ) { - // Get image information. - $info = $this->load_image_info( $media_id ); - if ( is_wp_error( $info ) ) { - return $info; - } - - // Update it with our modifier. - $info['meta'] = $modifier->apply_to_meta( $info['meta'] ); - - // Generate filename based on current attributes. - $target_file = $this->get_filename( $info['meta'] ); - - // Does the image already exist? - $image = $this->get_existing_image( $info, $target_file ); - if ( $image ) { - // Return the existing image. - return $image; - } - - // Try and load the image itself. - $image = $this->load_image( $media_id, $info ); - if ( is_wp_error( $image ) ) { - return $image; - } - - // Finally apply the modification. - $modified = $modifier->apply_to_image( $image['editor'] ); - if ( is_wp_error( $modified ) ) { - return $modified; - } - - // And save. - return $this->save_image( $image, $target_file, $info ); - } - - /** - * Loads an image for editing. - * - * @param integer $media_id Image ID. - * @return array|WP_Error The WP_Image_Editor and image path if successful, WP_Error otherwise. - */ - private function load_image( $media_id ) { - require_once ABSPATH . 'wp-admin/includes/image.php'; - - $image_path = get_attached_file( $media_id ); - - if ( empty( $image_path ) ) { - return new WP_Error( 'fileunknown', 'Unable to find original media file' ); - } - - $image_editor = wp_get_image_editor( $image_path ); - if ( ! $image_editor->load() ) { - return new WP_Error( 'fileload', 'Unable to load original media file' ); - } - - return array( - 'editor' => $image_editor, - 'path' => $image_path, - ); - } - - /** - * Gets the JSON response object for an image. - * - * @param integer $id Image ID. - * @return array Image JSON. - */ - private function get_image_as_json( $id ) { - return array( - 'media_id' => $id, - 'url' => wp_get_attachment_image_url( $id, 'original' ), - ); - } - - /** - * Checks for the existence of an image and if it exists, return the image. - * - * @param array $attachment Attachment with url to look up. - * @param string $target_file Target file name to look up. - * @return array|false Image JSON if exists, otherwise false. - */ - private function get_existing_image( $attachment, $target_file ) { - $url = str_replace( basename( $attachment['url'] ), $target_file, $attachment['url'] ); - - $new_id = attachment_url_to_postid( $url ); - if ( $new_id > 0 ) { - return $this->get_image_as_json( $new_id ); - } - - return false; - } - - /** - * Saves an edited image. - * - * @param array $image_edit Image path and editor to save. - * @param string $target_name Target file name to save as. - * @param array $attachment Attachment with metadata to apply. - * @return array|WP_Error Image JSON if successful, WP_Error otherwise - */ - private function save_image( $image_edit, $target_name, $attachment ) { - $filename = rtrim( dirname( $image_edit['path'] ), '/' ) . '/' . $target_name; - - // Save to disk. - $saved = $image_edit['editor']->save( $filename ); - - if ( is_wp_error( $saved ) ) { - return $saved; - } - - // Update attachment details. - $attachment_post = array( - 'guid' => $saved['path'], - 'post_mime_type' => $saved['mime-type'], - 'post_title' => pathinfo( $target_name, PATHINFO_FILENAME ), - 'post_content' => '', - 'post_status' => 'inherit', - ); - - // Add this as an attachment. - $attachment_id = wp_insert_attachment( $attachment_post, $saved['path'], 0 ); - if ( 0 === $attachment_id ) { - return new WP_Error( 'attachment', 'Unable to add image as attachment' ); - } - - // Generate thumbnails. - $metadata = wp_generate_attachment_metadata( $attachment_id, $saved['path'] ); - - // Store out meta data. - $metadata[ self::META_KEY ] = $attachment['meta']; - - wp_update_attachment_metadata( $attachment_id, $metadata ); - - return $this->get_image_as_json( $attachment_id ); - } - - /** - * Computes the filename based on metadata. - * - * @param array $meta Metadata for the image. - * @return string Name of the edited file. - */ - private function get_filename( $meta ) { - $parts = array(); - - foreach ( $this->all_modifiers as $modifier ) { - $parts[] = $modifier::get_filename( $meta ); - } - - $parts = array_filter( $parts ); - - if ( count( $parts ) > 0 ) { - return sprintf( '%s-%s', implode( '-', $parts ), $meta['original_name'] ); - } - - return $meta['original_name']; - } - - /** - * Loads image info. - * - * @param integer $media_id Image ID. - * @return array|WP_Error If successful image info, otherwise a WP_Error - */ - private function load_image_info( $media_id ) { - $attachment_info = wp_get_attachment_metadata( $media_id ); - $media_url = wp_get_attachment_image_url( $media_id, 'original' ); - - if ( ! $attachment_info || ! $media_url ) { - return new WP_Error( 'unknown', 'Unable to get meta information for file' ); - } - - $default_meta = array(); - foreach ( $this->all_modifiers as $modifier ) { - $default_meta = array_merge( $default_meta, $modifier::get_default_meta() ); - } - - $info = array( - 'url' => $media_url, - 'media_id' => $media_id, - 'meta' => array_merge( - $default_meta, - array( 'original_name' => basename( $media_url ) ) - ), - ); - - if ( isset( $attachment_info[ self::META_KEY ] ) ) { - $info['meta'] = array_merge( $info['meta'], $attachment_info[ self::META_KEY ] ); - } - - return $info; - } -} - -/** - * Abstract class for image modifiers. Any modifier to an image should implement this. - * - * @abstract - */ -abstract class Image_Editor_Modifier { - - /** - * Update the image metadata with the modifier. - * - * @abstract - * @access public - * - * @param array $meta Metadata to update. - * @return array Updated metadata. - */ - abstract public function apply_to_meta( $meta ); - - /** - * Apply the modifier to the image - * - * @abstract - * @access public - * - * @param WP_Image_Editor $image Image editor. - * @return bool|WP_Error True on success, WP_Error object or false on failure. - */ - abstract public function apply_to_image( $image ); - - /** - * Gets the new filename based on metadata. - * - * @abstract - * @access public - * - * @param array $meta Image metadata. - * @return string Filename for the edited image. - */ - abstract public static function get_filename( $meta ); - - /** - * Gets the default metadata for an image modifier. - * - * @abstract - * @access public - * - * @return array Default metadata. - */ - abstract public static function get_default_meta(); -} diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 2118143b19777..a61b2d92fbdd9 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -15,7 +15,6 @@ import { BlockControls, BlockIcon, MediaPlaceholder, - MediaReplaceFlow, __experimentalBlock as Block, } from '@wordpress/block-editor'; import { useEffect, useRef } from '@wordpress/element'; @@ -223,17 +222,6 @@ export function ImageEdit( { value={ align } onChange={ updateAlignment } /> - { url && ( - - ) } ); const src = isExternal ? url : undefined; @@ -283,6 +271,9 @@ export function ImageEdit( { isSelected={ isSelected } insertBlocksAfter={ insertBlocksAfter } onReplace={ onReplace } + onSelectImage={ onSelectImage } + onSelectURL={ onSelectURL } + onUploadError={ onUploadError } containerRef={ ref } /> ) } diff --git a/packages/block-library/src/image/editor.scss b/packages/block-library/src/image/editor.scss index d4fce2610c703..89c45696dcc3c 100644 --- a/packages/block-library/src/image/editor.scss +++ b/packages/block-library/src/image/editor.scss @@ -5,6 +5,7 @@ figure.wp-block-image:not(.wp-block) { .wp-block-image { position: relative; + .is-applying img, &.is-transient img { opacity: 0.3; } @@ -71,44 +72,6 @@ figure.wp-block-image:not(.wp-block) { text-align: center; } -// Working State. -.richimage__working { - position: relative; - - .richimage__working-spinner { - position: absolute; - z-index: 1; - left: 50%; - top: calc(50% - #{ $grid-unit-60 }); - transform: translate(-50%, -50%); - } - - img { - opacity: 0.6; - transition: all 0.4s ease; // Make flips smooth. - } - - &.richimage__working__flipv img { - transform: scale(1, -1); - } - - &.richimage__working__fliph img { - transform: scale(-1, 1); - } -} - -// Without this the toolbar buttons gain padding, making them too tall and breaking the toolbar -// This only happens during the processing state as we change the dropdowns into buttons to disable the dropdowns -.richimage-toolbar__working { - padding: 0 6px; -} - -.richimage-crop { - .components-resizable-box__handle { - display: block; - } -} - .richimage__crop-area { position: relative; max-width: 100%; diff --git a/packages/block-library/src/image/image-editor.js b/packages/block-library/src/image/image-editor.js index 6dfaa4fba7fa4..b56cdb4ae4822 100644 --- a/packages/block-library/src/image/image-editor.js +++ b/packages/block-library/src/image/image-editor.js @@ -2,21 +2,17 @@ * External dependencies */ -import classnames from 'classnames'; import Cropper from 'react-easy-crop'; +import classnames from 'classnames'; /** * WordPress dependencies */ import { BlockControls } from '@wordpress/block-editor'; -import { useState, useEffect } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import { - rotateLeft as rotateLeftIcon, rotateRight as rotateRightIcon, - flipHorizontal as flipHorizontalIcon, - flipVertical as flipVerticalIcon, - crop as cropIcon, aspectRatio as aspectRatioIcon, } from '@wordpress/icons'; import { @@ -33,30 +29,11 @@ import { __ } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; import apiFetch from '@wordpress/api-fetch'; -const ROTATE_STEP = 90; -const DEFAULT_CROP = { - unit: '%', - x: 25, - y: 25, - width: 50, - height: 50, -}; const MIN_ZOOM = 1; const MAX_ZOOM = 3; -const ZOOM_STEP = 0.1; +const ZOOM_STEP = 0.01; const POPOVER_PROPS = { position: 'bottom right' }; -function richImageRequest( id, action, attrs ) { - return apiFetch( { - path: `__experimental/richimage/${ id }/${ action }`, - headers: { - 'Content-type': 'application/json', - }, - method: 'POST', - body: JSON.stringify( attrs ), - } ); -} - function AspectGroup( { aspectRatios, isDisabled, label, onClick } ) { return ( @@ -160,43 +137,52 @@ export default function ImageEditor( { id, url, setAttributes, - isSelected, naturalWidth, naturalHeight, width, height, clientWidth, - children, + setIsEditingImage, } ) { const { createErrorNotice } = useDispatch( 'core/notices' ); - const [ isCropping, setIsCropping ] = useState( false ); - const [ inProgress, setIsProgress ] = useState( null ); + const [ inProgress, setIsProgress ] = useState( false ); const [ crop, setCrop ] = useState( null ); const [ position, setPosition ] = useState( { x: 0, y: 0 } ); const [ zoom, setZoom ] = useState( 1 ); - const [ aspect, setAspect ] = useState( 4 / 3 ); + const [ aspect, setAspect ] = useState( naturalWidth / naturalHeight ); + const [ rotation, setRotation ] = useState( 0 ); + const [ editedUrl, setEditedUrl ] = useState(); - // Cancel cropping on deselect. - useEffect( () => { - if ( ! isSelected ) { - setIsCropping( false ); - } - }, [ isSelected ] ); + const editedWidth = width; + let editedHeight = height || ( clientWidth * naturalHeight ) / naturalWidth; + + if ( rotation % 180 === 90 ) { + editedHeight = ( clientWidth * naturalWidth ) / naturalHeight; + } - function adjustImage( action, attrs ) { - setIsProgress( action ); + function apply() { + setIsProgress( true ); - richImageRequest( id, action, attrs ) - .then( ( response ) => { - setIsProgress( null ); - setIsCropping( false ); + const attrs = crop; - if ( response.media_id && response.media_id !== id ) { - setAttributes( { - id: response.media_id, - url: response.url, - } ); - } + if ( rotation > 0 ) { + attrs.rotation = rotation; + } + + apiFetch( { + path: `__experimental/richimage/${ id }/apply`, + headers: { + 'Content-type': 'application/json', + }, + method: 'POST', + body: JSON.stringify( attrs ), + } ) + .then( ( response ) => { + setAttributes( { + id: response.media_id, + url: response.url, + height: height && width ? width / aspect : undefined, + } ); } ) .catch( () => { createErrorNotice( @@ -208,177 +194,127 @@ export default function ImageEditor( { type: 'snackbar', } ); - setIsProgress( null ); - setIsCropping( false ); + } ) + .finally( () => { + setIsProgress( false ); + setIsEditingImage( false ); } ); } - function cropImage() { - adjustImage( 'crop', { - crop_x: crop.x, - crop_y: crop.y, - crop_width: crop.width, - crop_height: crop.height, - } ); - } + function rotate() { + const angle = ( rotation + 90 ) % 360; + + if ( angle === 0 ) { + setEditedUrl(); + setRotation( angle ); + setAspect( 1 / aspect ); + return; + } - const classes = classnames( { - richimage__working: inProgress !== null, - [ 'richimage__working__' + inProgress ]: inProgress !== null, - } ); + function editImage( event ) { + const canvas = document.createElement( 'canvas' ); + + let translateX = 0; + let translateY = 0; + + if ( angle % 180 ) { + canvas.width = event.target.height; + canvas.height = event.target.width; + } else { + canvas.width = event.target.width; + canvas.height = event.target.height; + } + + if ( angle === 90 || angle === 180 ) { + translateX = canvas.width; + } + + if ( angle === 270 || angle === 180 ) { + translateY = canvas.height; + } + + const context = canvas.getContext( '2d' ); + + context.translate( translateX, translateY ); + context.rotate( ( angle * Math.PI ) / 180 ); + context.drawImage( event.target, 0, 0 ); + + canvas.toBlob( ( blob ) => { + setEditedUrl( URL.createObjectURL( blob ) ); + setRotation( angle ); + setAspect( 1 / aspect ); + } ); + } + + const el = new window.Image(); + el.src = url; + el.onload = editImage; + } return ( <> -
- { inProgress && ( -
- -
- ) } - { isCropping ? ( - <> -
- -
- - - ) : ( - children - ) } +
+ + { inProgress && }
+ { ! inProgress && ( + + ) } - { ! isCropping && ( - - - { ( toggleProps ) => ( - - ) } - - - { ( toggleProps ) => ( - { - adjustImage( 'flip', { - direction: 'vertical', - } ); - }, - }, - { - icon: flipHorizontalIcon, - title: __( 'Flip horizontal' ), - isDisabled: inProgress, - onClick: () => { - adjustImage( 'flip', { - direction: 'horizontal', - } ); - }, - }, - ] } - /> - ) } - - { - setIsCropping( ( prev ) => ! prev ); - setCrop( DEFAULT_CROP ); - } } - /> - - ) } - { isCropping && ( - <> - - - { ( toggleProps ) => ( - - ) } - - - - - { __( 'Apply' ) } - - { - setIsCropping( false ); - } } - > - { __( 'Cancel' ) } - - - - ) } + + + + + + { ( toggleProps ) => ( + + ) } + + + + + { __( 'Apply' ) } + + setIsEditingImage( false ) }> + { __( 'Cancel' ) } + + ); diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index bde36660bf606..144e6e51e3bb5 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -15,6 +15,7 @@ import { TextareaControl, TextControl, ToolbarGroup, + ToolbarButton, } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -25,6 +26,7 @@ import { RichText, __experimentalImageSizeControl as ImageSizeControl, __experimentalImageURLInputUI as ImageURLInputUI, + MediaReplaceFlow, } from '@wordpress/block-editor'; import { useEffect, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; @@ -41,7 +43,7 @@ import ImageEditor from './image-editor'; /** * Module constants */ -import { MIN_SIZE } from './constants'; +import { MIN_SIZE, ALLOWED_MEDIA_TYPES } from './constants'; function getFilename( url ) { const path = getPath( url ); @@ -71,6 +73,9 @@ export default function Image( { isSelected, insertBlocksAfter, onReplace, + onSelectImage, + onSelectURL, + onUploadError, containerRef, } ) { const image = useSelect( @@ -99,6 +104,7 @@ export default function Image( { const [ captionFocused, setCaptionFocused ] = useState( false ); const isWideAligned = includes( [ 'wide', 'full' ], align ); const [ { naturalWidth, naturalHeight }, setNaturalSize ] = useState( {} ); + const [ isEditingImage, setIsEditingImage ] = useState( false ); const clientWidth = useClientWidth( containerRef, [ align ] ); const isResizable = ! isWideAligned && isLargeViewport; const imageSizeOptions = map( @@ -175,10 +181,22 @@ export default function Image( { } ); } + useEffect( () => { + if ( ! isSelected ) { + setIsEditingImage( false ); + } + }, [ isSelected ] ); + + const canEditImage = + __experimentalEnableRichImageEditing && + id && + naturalWidth && + naturalHeight; + const controls = ( <> - { url && ( + { ! isEditingImage && ( ) } + { canEditImage && ! isEditingImage && ( + + setIsEditingImage( true ) } + > + { __( 'Crop' ) } + + + ) } + { ! isEditingImage && ( + + ) } @@ -267,7 +305,6 @@ export default function Image( { // should direct focus to block. /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ <> - { controls } { + ); + } else if ( ! isResizable || ! imageWidthWithinContainer ) { img =
{ img }
; } else { const currentWidth = width || imageWidthWithinContainer; @@ -380,26 +431,9 @@ export default function Image( { ); } - if ( __experimentalEnableRichImageEditing ) { - img = ( - - { img } - - ); - } - return ( <> + { controls } { img } { ( ! RichText.isEmpty( caption ) || isSelected ) && (