From b1a4a783da9afeef2341bded62f9a026f674286e Mon Sep 17 00:00:00 2001 From: Ella van Durpe Date: Thu, 18 Jun 2020 20:23:55 +0300 Subject: [PATCH 1/9] Image editing: batch editing in cropper component --- lib/class-wp-rest-image-editor-controller.php | 163 +++++++++++ packages/block-library/src/image/editor.scss | 9 - .../block-library/src/image/image-editor.js | 269 +++++------------- packages/block-library/src/image/image.js | 55 ++-- 4 files changed, 272 insertions(+), 224 deletions(-) diff --git a/lib/class-wp-rest-image-editor-controller.php b/lib/class-wp-rest-image-editor-controller.php index 8be0dd425834e..f686f86bd8ec3 100644 --- a/lib/class-wp-rest-image-editor-controller.php +++ b/lib/class-wp-rest-image-editor-controller.php @@ -116,6 +116,72 @@ public function register_routes() { ), ) ); + + register_rest_route( + $this->namespace, + $this->rest_base . '/apply', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'apply_edits' ), + 'permission_callback' => array( $this, 'permission_callback' ), + 'args' => array( + array( + 'type' => 'object', + 'properties' => array( + 'crop' => array( + 'type' => 'object', + 'properties' => array( + 'x' => array( + 'type' => 'integer', + 'minimum' => 0, + 'required' => true, + ), + 'y' => array( + 'type' => 'integer', + 'minimum' => 0, + 'required' => true, + ), + 'w' => array( + 'type' => 'integer', + 'minimum' => 1, + 'required' => true, + ), + 'h' => array( + 'type' => 'integer', + 'minimum' => 1, + 'required' => true, + ), + ), + ), + 'rotate' => array( + 'type' => 'object', + 'properties' => array( + 'angle' => array( + 'type' => 'integer', + 'required' => true, + ), + ), + ), + 'flip' => array( + 'type' => 'object', + 'properties' => array( + 'horizontal' => array( + 'type' => 'boolean', + 'required' => true, + ), + 'vertical' => array( + 'type' => 'boolean', + 'required' => true, + ), + ), + ), + ), + ), + ), + ), + ) + ); } /** @@ -179,4 +245,101 @@ public function crop_image( $request ) { return $this->editor->modify_image( $request['media_id'], $modifier ); } + + /** + * Applies all edits in one go. + * + * @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 apply_edits( $request ) { + require_once ABSPATH . 'wp-admin/includes/image.php'; + + $params = $request->get_params(); + + $media_id = $params['media_id']; + + // Get image information. + $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' ); + } + + $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' ); + } + + $size = $image_editor->get_size(); + + $crop_x = round( ( $size['width'] * $params['crop']['x'] ) / 100.0 ); + $crop_y = round( ( $size['height'] * $params['crop']['y'] ) / 100.0 ); + $width = round( ( $size['width'] * $params['crop']['width'] ) / 100.0 ); + $height = round( ( $size['height'] * $params['crop']['height'] ) / 100.0 ); + + // Finally apply the modifications. + if ( isset( $params['crop'] ) ) { + $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/packages/block-library/src/image/editor.scss b/packages/block-library/src/image/editor.scss index d4fce2610c703..7852062169ea2 100644 --- a/packages/block-library/src/image/editor.scss +++ b/packages/block-library/src/image/editor.scss @@ -85,15 +85,6 @@ figure.wp-block-image:not(.wp-block) { 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); } } diff --git a/packages/block-library/src/image/image-editor.js b/packages/block-library/src/image/image-editor.js index 6dfaa4fba7fa4..59865b9797804 100644 --- a/packages/block-library/src/image/image-editor.js +++ b/packages/block-library/src/image/image-editor.js @@ -2,7 +2,6 @@ * External dependencies */ -import classnames from 'classnames'; import Cropper from 'react-easy-crop'; /** @@ -10,13 +9,9 @@ import Cropper from 'react-easy-crop'; */ 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,22 +28,14 @@ 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 POPOVER_PROPS = { position: 'bottom right' }; -function richImageRequest( id, action, attrs ) { +function richImageRequest( id, attrs ) { return apiFetch( { - path: `__experimental/richimage/${ id }/${ action }`, + path: `__experimental/richimage/${ id }/apply`, headers: { 'Content-type': 'application/json', }, @@ -160,36 +147,28 @@ 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 [ rotation, setRotation ] = useState( 0 ); - // Cancel cropping on deselect. - useEffect( () => { - if ( ! isSelected ) { - setIsCropping( false ); - } - }, [ isSelected ] ); + function apply() { + setIsProgress( true ); - function adjustImage( action, attrs ) { - setIsProgress( action ); - - richImageRequest( id, action, attrs ) + richImageRequest( id, { crop, rotation } ) .then( ( response ) => { - setIsProgress( null ); - setIsCropping( false ); + setIsProgress( false ); + setIsEditingImage( false ); if ( response.media_id && response.media_id !== id ) { setAttributes( { @@ -208,177 +187,79 @@ export default function ImageEditor( { type: 'snackbar', } ); - setIsProgress( null ); - setIsCropping( false ); + 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() { + setRotation( ( value ) => ( value + 90 ) % 360 ); } - const classes = classnames( { - richimage__working: inProgress !== null, - [ 'richimage__working__' + inProgress ]: inProgress !== null, - } ); - return ( <> -
- { inProgress && ( -
- -
- ) } - { isCropping ? ( - <> -
- -
- - - ) : ( - children - ) } + { 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' ) } + + ); diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index bde36660bf606..f113f292fbfe7 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'; @@ -99,6 +100,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 +177,16 @@ export default function Image( { } ); } + useEffect( () => { + if ( ! isSelected ) { + setIsEditingImage( false ); + } + }, [ isSelected ] ); + const controls = ( <> - { url && ( + { ! isEditingImage && ( ) } + + + setIsEditingImage( ( value ) => ! value ) + } + > + { isEditingImage ? __( 'Cancel' ) : __( 'Edit' ) } + + @@ -267,7 +284,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 +410,9 @@ export default function Image( { ); } - if ( __experimentalEnableRichImageEditing ) { - img = ( - - { img } - - ); - } - return ( <> + { controls } { img } { ( ! RichText.isEmpty( caption ) || isSelected ) && ( Date: Fri, 19 Jun 2020 11:40:16 +0300 Subject: [PATCH 2/9] For rotation, set new image url because rotation is limited in cropper --- .../block-library/src/image/image-editor.js | 67 ++++++++++++++++--- packages/block-library/src/image/image.js | 7 +- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/packages/block-library/src/image/image-editor.js b/packages/block-library/src/image/image-editor.js index 59865b9797804..91943f97c14ba 100644 --- a/packages/block-library/src/image/image-editor.js +++ b/packages/block-library/src/image/image-editor.js @@ -159,8 +159,16 @@ export default function ImageEditor( { 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(); + + const editedWidth = width; + let editedHeight = height || ( clientWidth * naturalHeight ) / naturalWidth; + + if ( rotation % 180 === 90 ) { + editedHeight = ( clientWidth * naturalWidth ) / naturalHeight; + } function apply() { setIsProgress( true ); @@ -193,7 +201,53 @@ export default function ImageEditor( { } function rotate() { - setRotation( ( value ) => ( value + 90 ) % 360 ); + const angle = ( rotation + 90 ) % 360; + + if ( angle === 0 ) { + setEditedUrl(); + setRotation( angle ); + setAspect( 1 / aspect ); + return; + } + + 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 ( @@ -206,20 +260,17 @@ export default function ImageEditor( {
Date: Fri, 19 Jun 2020 12:06:12 +0300 Subject: [PATCH 3/9] Don' add editor for images not hosted on the server --- packages/block-library/src/image/image.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index b1577729e71ff..c3c274cfb52af 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -200,15 +200,17 @@ export default function Image( { /> ) } - - - setIsEditingImage( ( value ) => ! value ) - } - > - { isEditingImage ? __( 'Cancel' ) : __( 'Edit' ) } - - + { id && ( + + + setIsEditingImage( ( value ) => ! value ) + } + > + { isEditingImage ? __( 'Cancel' ) : __( 'Edit' ) } + + + ) } @@ -318,6 +320,7 @@ export default function Image( { if ( __experimentalEnableRichImageEditing && isEditingImage && + id && naturalWidth && naturalHeight ) { From 159d13b606b508b26903c77bdddbcc5a9a6dd3ea Mon Sep 17 00:00:00 2001 From: Ella van Durpe Date: Fri, 19 Jun 2020 13:29:09 +0300 Subject: [PATCH 4/9] Fix loading indicator --- packages/block-library/src/image/editor.scss | 30 +--------------- .../block-library/src/image/image-editor.js | 34 ++++++++++--------- 2 files changed, 19 insertions(+), 45 deletions(-) diff --git a/packages/block-library/src/image/editor.scss b/packages/block-library/src/image/editor.scss index 7852062169ea2..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,35 +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; - } -} - -// 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 91943f97c14ba..0d5d9f8eab391 100644 --- a/packages/block-library/src/image/image-editor.js +++ b/packages/block-library/src/image/image-editor.js @@ -3,6 +3,7 @@ */ import Cropper from 'react-easy-crop'; +import classnames from 'classnames'; /** * WordPress dependencies @@ -252,13 +253,10 @@ export default function ImageEditor( { return ( <> - { inProgress && ( -
- -
- ) }
+ { inProgress && }
- + { ! inProgress && ( + + ) } @@ -307,7 +309,7 @@ export default function ImageEditor( { - + { __( 'Apply' ) } From 00389c31ae812cc129a772ea74ff54dccbc16ed6 Mon Sep 17 00:00:00 2001 From: Ella van Durpe Date: Fri, 19 Jun 2020 15:44:45 +0300 Subject: [PATCH 5/9] Fix image edit without rotate --- packages/block-library/src/image/image-editor.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/image/image-editor.js b/packages/block-library/src/image/image-editor.js index 0d5d9f8eab391..1513670daa06d 100644 --- a/packages/block-library/src/image/image-editor.js +++ b/packages/block-library/src/image/image-editor.js @@ -174,7 +174,13 @@ export default function ImageEditor( { function apply() { setIsProgress( true ); - richImageRequest( id, { crop, rotation } ) + const attrs = { crop }; + + if ( rotation > 0 ) { + attrs.rotation = rotation; + } + + richImageRequest( id, attrs ) .then( ( response ) => { setIsProgress( false ); setIsEditingImage( false ); From d0b20bfd8d5c878cbb33f6fe5a7241a1de197a68 Mon Sep 17 00:00:00 2001 From: Ella van Durpe Date: Fri, 19 Jun 2020 17:24:05 +0300 Subject: [PATCH 6/9] Reorg buttons --- packages/block-library/src/image/edit.js | 15 ++----- .../block-library/src/image/image-editor.js | 3 ++ packages/block-library/src/image/image.js | 39 ++++++++++++------- 3 files changed, 32 insertions(+), 25 deletions(-) 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/image-editor.js b/packages/block-library/src/image/image-editor.js index 1513670daa06d..4f65723ad0477 100644 --- a/packages/block-library/src/image/image-editor.js +++ b/packages/block-library/src/image/image-editor.js @@ -318,6 +318,9 @@ export default function ImageEditor( { { __( 'Apply' ) } + setIsEditingImage( false ) }> + { __( 'Cancel' ) } + diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index c3c274cfb52af..144e6e51e3bb5 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -26,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'; @@ -42,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 ); @@ -72,6 +73,9 @@ export default function Image( { isSelected, insertBlocksAfter, onReplace, + onSelectImage, + onSelectURL, + onUploadError, containerRef, } ) { const image = useSelect( @@ -183,6 +187,12 @@ export default function Image( { } }, [ isSelected ] ); + const canEditImage = + __experimentalEnableRichImageEditing && + id && + naturalWidth && + naturalHeight; + const controls = ( <> @@ -200,17 +210,26 @@ export default function Image( { /> ) } - { id && ( + { canEditImage && ! isEditingImage && ( - setIsEditingImage( ( value ) => ! value ) - } + onClick={ () => setIsEditingImage( true ) } > - { isEditingImage ? __( 'Cancel' ) : __( 'Edit' ) } + { __( 'Crop' ) } ) } + { ! isEditingImage && ( + + ) } @@ -317,13 +336,7 @@ export default function Image( { : naturalHeight; } - if ( - __experimentalEnableRichImageEditing && - isEditingImage && - id && - naturalWidth && - naturalHeight - ) { + if ( canEditImage && isEditingImage ) { img = ( Date: Fri, 19 Jun 2020 18:38:37 +0300 Subject: [PATCH 7/9] Recalculate height after aspect ratio change --- packages/block-library/src/image/image-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/image/image-editor.js b/packages/block-library/src/image/image-editor.js index 4f65723ad0477..f57ddd2fb0a1c 100644 --- a/packages/block-library/src/image/image-editor.js +++ b/packages/block-library/src/image/image-editor.js @@ -189,6 +189,7 @@ export default function ImageEditor( { setAttributes( { id: response.media_id, url: response.url, + height: width / aspect, } ); } } ) @@ -279,7 +280,6 @@ export default function ImageEditor( { onCropChange={ setPosition } onCropComplete={ setCrop } onZoomChange={ setZoom } - onRotationChange={ setRotation } /> { inProgress && }
From 55edeae8aaf003c9fbd74ed126fd4f7fa737afd9 Mon Sep 17 00:00:00 2001 From: Ella van Durpe Date: Sun, 21 Jun 2020 22:02:15 +0300 Subject: [PATCH 8/9] Remove unused endpoints --- lib/class-wp-rest-image-editor-controller.php | 212 ++----------- 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 ------------------ .../block-library/src/image/image-editor.js | 41 +-- 6 files changed, 43 insertions(+), 862 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 f686f86bd8ec3..a794fdb0b861a 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,141 +36,35 @@ public function __construct() { public function register_routes() { register_rest_route( $this->namespace, - $this->rest_base . '/rotate', + $this->rest_base . '/apply', array( array( 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'rotate_image' ), + 'callback' => array( $this, 'apply_edits' ), 'permission_callback' => array( $this, 'permission_callback' ), 'args' => array( - 'angle' => array( - 'description' => __( 'Rotation angle', 'gutenberg' ), - 'type' => 'integer', - 'required' => true, + 'x' => array( + 'type' => 'float', + 'minimum' => 0, + 'required' => true, ), - ), - ), - ) - ); - - register_rest_route( - $this->namespace, - $this->rest_base . '/flip', - array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'flip_image' ), - 'permission_callback' => array( $this, 'permission_callback' ), - 'args' => array( - 'direction' => array( - 'description' => __( 'Flip direction', 'gutenberg' ), - 'type' => 'string', - 'enum' => array( 'vertical', 'horizontal' ), - 'required' => true, + 'y' => 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, - ), - '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, - ), - ), - ), - ) - ); - - register_rest_route( - $this->namespace, - $this->rest_base . '/apply', - array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'apply_edits' ), - 'permission_callback' => array( $this, 'permission_callback' ), - 'args' => array( - array( - 'type' => 'object', - 'properties' => array( - 'crop' => array( - 'type' => 'object', - 'properties' => array( - 'x' => array( - 'type' => 'integer', - 'minimum' => 0, - 'required' => true, - ), - 'y' => array( - 'type' => 'integer', - 'minimum' => 0, - 'required' => true, - ), - 'w' => array( - 'type' => 'integer', - 'minimum' => 1, - 'required' => true, - ), - 'h' => array( - 'type' => 'integer', - 'minimum' => 1, - 'required' => true, - ), - ), - ), - 'rotate' => array( - 'type' => 'object', - 'properties' => array( - 'angle' => array( - 'type' => 'integer', - 'required' => true, - ), - ), - ), - 'flip' => array( - 'type' => 'object', - 'properties' => array( - 'horizontal' => array( - 'type' => 'boolean', - 'required' => true, - ), - 'vertical' => array( - 'type' => 'boolean', - 'required' => true, - ), - ), - ), - ), + 'rotation' => array( + 'type' => 'integer', ), ), ), @@ -202,37 +90,7 @@ public function permission_callback( $request ) { } /** - * Rotates 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 rotate_image( $request ) { - $modifier = new Image_Editor_Rotate( $request['angle'] ); - - return $this->editor->modify_image( $request['media_id'], $modifier ); - } - - /** - * 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'] ); - - return $this->editor->modify_image( $request['media_id'], $modifier ); - } - - /** - * Crops an image. + * Applies all edits in one go. * * @since 7.x ? * @access public @@ -240,21 +98,6 @@ public function flip_image( $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 crop_image( $request ) { - $modifier = new Image_Editor_Crop( $request['crop_x'], $request['crop_y'], $request['crop_width'], $request['crop_height'] ); - - return $this->editor->modify_image( $request['media_id'], $modifier ); - } - - /** - * Applies all edits in one go. - * - * @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 apply_edits( $request ) { require_once ABSPATH . 'wp-admin/includes/image.php'; @@ -289,15 +132,12 @@ public function apply_edits( $request ) { $size = $image_editor->get_size(); - $crop_x = round( ( $size['width'] * $params['crop']['x'] ) / 100.0 ); - $crop_y = round( ( $size['height'] * $params['crop']['y'] ) / 100.0 ); - $width = round( ( $size['width'] * $params['crop']['width'] ) / 100.0 ); - $height = round( ( $size['height'] * $params['crop']['height'] ) / 100.0 ); - // Finally apply the modifications. - if ( isset( $params['crop'] ) ) { - $image_editor->crop( $crop_x, $crop_y, $width, $height ); - } + $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'] ); 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/image-editor.js b/packages/block-library/src/image/image-editor.js index f57ddd2fb0a1c..b56cdb4ae4822 100644 --- a/packages/block-library/src/image/image-editor.js +++ b/packages/block-library/src/image/image-editor.js @@ -31,20 +31,9 @@ import apiFetch from '@wordpress/api-fetch'; 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, attrs ) { - return apiFetch( { - path: `__experimental/richimage/${ id }/apply`, - headers: { - 'Content-type': 'application/json', - }, - method: 'POST', - body: JSON.stringify( attrs ), - } ); -} - function AspectGroup( { aspectRatios, isDisabled, label, onClick } ) { return ( @@ -174,24 +163,26 @@ export default function ImageEditor( { function apply() { setIsProgress( true ); - const attrs = { crop }; + const attrs = crop; if ( rotation > 0 ) { attrs.rotation = rotation; } - richImageRequest( id, attrs ) + apiFetch( { + path: `__experimental/richimage/${ id }/apply`, + headers: { + 'Content-type': 'application/json', + }, + method: 'POST', + body: JSON.stringify( attrs ), + } ) .then( ( response ) => { - setIsProgress( false ); - setIsEditingImage( false ); - - if ( response.media_id && response.media_id !== id ) { - setAttributes( { - id: response.media_id, - url: response.url, - height: width / aspect, - } ); - } + setAttributes( { + id: response.media_id, + url: response.url, + height: height && width ? width / aspect : undefined, + } ); } ) .catch( () => { createErrorNotice( @@ -203,6 +194,8 @@ export default function ImageEditor( { type: 'snackbar', } ); + } ) + .finally( () => { setIsProgress( false ); setIsEditingImage( false ); } ); From 1926307ca81a9fa76ff7d4ecbeb1c36f4b6b5456 Mon Sep 17 00:00:00 2001 From: Ella van Durpe Date: Sun, 21 Jun 2020 22:30:01 +0300 Subject: [PATCH 9/9] Fix php lint errors --- lib/class-wp-rest-image-editor-controller.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-rest-image-editor-controller.php b/lib/class-wp-rest-image-editor-controller.php index a794fdb0b861a..e572f665bb5c8 100644 --- a/lib/class-wp-rest-image-editor-controller.php +++ b/lib/class-wp-rest-image-editor-controller.php @@ -43,28 +43,28 @@ public function register_routes() { 'callback' => array( $this, 'apply_edits' ), 'permission_callback' => array( $this, 'permission_callback' ), 'args' => array( - 'x' => array( + 'x' => array( 'type' => 'float', 'minimum' => 0, 'required' => true, ), - 'y' => array( + 'y' => array( 'type' => 'float', 'minimum' => 0, 'required' => true, ), - 'width' => array( + 'width' => array( 'type' => 'float', 'minimum' => 1, 'required' => true, ), - 'height' => array( + 'height' => array( 'type' => 'float', 'minimum' => 1, 'required' => true, ), 'rotation' => array( - 'type' => 'integer', + 'type' => 'integer', ), ), ),