1<?php
2/**
3 * WordPress Image Editor
4 *
5 * @package WordPress
6 * @subpackage Administration
7 */
8
9/**
10 * Loads the WP image-editing interface.
11 *
12 * @since 2.9.0
13 *
14 * @param int          $post_id Attachment post ID.
15 * @param false|object $msg     Optional. Message to display for image editor updates or errors.
16 *                              Default false.
17 */
18function wp_image_editor( $post_id, $msg = false ) {
19	$nonce     = wp_create_nonce( "image_editor-$post_id" );
20	$meta      = wp_get_attachment_metadata( $post_id );
21	$thumb     = image_get_intermediate_size( $post_id, 'thumbnail' );
22	$sub_sizes = isset( $meta['sizes'] ) && is_array( $meta['sizes'] );
23	$note      = '';
24
25	if ( isset( $meta['width'], $meta['height'] ) ) {
26		$big = max( $meta['width'], $meta['height'] );
27	} else {
28		die( __( 'Image data does not exist. Please re-upload the image.' ) );
29	}
30
31	$sizer = $big > 400 ? 400 / $big : 1;
32
33	$backup_sizes = get_post_meta( $post_id, '_wp_attachment_backup_sizes', true );
34	$can_restore  = false;
35	if ( ! empty( $backup_sizes ) && isset( $backup_sizes['full-orig'], $meta['file'] ) ) {
36		$can_restore = wp_basename( $meta['file'] ) !== $backup_sizes['full-orig']['file'];
37	}
38
39	if ( $msg ) {
40		if ( isset( $msg->error ) ) {
41			$note = "<div class='notice notice-error' tabindex='-1' role='alert'><p>$msg->error</p></div>";
42		} elseif ( isset( $msg->msg ) ) {
43			$note = "<div class='notice notice-success' tabindex='-1' role='alert'><p>$msg->msg</p></div>";
44		}
45	}
46
47	?>
48	<div class="imgedit-wrap wp-clearfix">
49	<div id="imgedit-panel-<?php echo $post_id; ?>">
50
51	<div class="imgedit-panel-content wp-clearfix">
52		<?php echo $note; ?>
53		<div class="imgedit-menu wp-clearfix">
54			<button type="button" onclick="imageEdit.handleCropToolClick( <?php echo "$post_id, '$nonce'"; ?>, this )" class="imgedit-crop button disabled" disabled><?php esc_html_e( 'Crop' ); ?></button>
55			<?php
56
57			// On some setups GD library does not provide imagerotate() - Ticket #11536.
58			if ( wp_image_editor_supports(
59				array(
60					'mime_type' => get_post_mime_type( $post_id ),
61					'methods'   => array( 'rotate' ),
62				)
63			) ) {
64				$note_no_rotate = '';
65				?>
66				<button type="button" class="imgedit-rleft button" onclick="imageEdit.rotate( 90, <?php echo "$post_id, '$nonce'"; ?>, this)"><?php esc_html_e( 'Rotate left' ); ?></button>
67				<button type="button" class="imgedit-rright button" onclick="imageEdit.rotate(-90, <?php echo "$post_id, '$nonce'"; ?>, this)"><?php esc_html_e( 'Rotate right' ); ?></button>
68				<?php
69			} else {
70				$note_no_rotate = '<p class="note-no-rotate"><em>' . __( 'Image rotation is not supported by your web host.' ) . '</em></p>';
71				?>
72				<button type="button" class="imgedit-rleft button disabled" disabled></button>
73				<button type="button" class="imgedit-rright button disabled" disabled></button>
74			<?php } ?>
75
76			<button type="button" onclick="imageEdit.flip(1, <?php echo "$post_id, '$nonce'"; ?>, this)" class="imgedit-flipv button"><?php esc_html_e( 'Flip vertical' ); ?></button>
77			<button type="button" onclick="imageEdit.flip(2, <?php echo "$post_id, '$nonce'"; ?>, this)" class="imgedit-fliph button"><?php esc_html_e( 'Flip horizontal' ); ?></button>
78
79			<br class="imgedit-undo-redo-separator" />
80			<button type="button" id="image-undo-<?php echo $post_id; ?>" onclick="imageEdit.undo(<?php echo "$post_id, '$nonce'"; ?>, this)" class="imgedit-undo button disabled" disabled><?php esc_html_e( 'Undo' ); ?></button>
81			<button type="button" id="image-redo-<?php echo $post_id; ?>" onclick="imageEdit.redo(<?php echo "$post_id, '$nonce'"; ?>, this)" class="imgedit-redo button disabled" disabled><?php esc_html_e( 'Redo' ); ?></button>
82			<?php echo $note_no_rotate; ?>
83		</div>
84
85		<input type="hidden" id="imgedit-sizer-<?php echo $post_id; ?>" value="<?php echo $sizer; ?>" />
86		<input type="hidden" id="imgedit-history-<?php echo $post_id; ?>" value="" />
87		<input type="hidden" id="imgedit-undone-<?php echo $post_id; ?>" value="0" />
88		<input type="hidden" id="imgedit-selection-<?php echo $post_id; ?>" value="" />
89		<input type="hidden" id="imgedit-x-<?php echo $post_id; ?>" value="<?php echo isset( $meta['width'] ) ? $meta['width'] : 0; ?>" />
90		<input type="hidden" id="imgedit-y-<?php echo $post_id; ?>" value="<?php echo isset( $meta['height'] ) ? $meta['height'] : 0; ?>" />
91
92		<div id="imgedit-crop-<?php echo $post_id; ?>" class="imgedit-crop-wrap">
93		<img id="image-preview-<?php echo $post_id; ?>" onload="imageEdit.imgLoaded('<?php echo $post_id; ?>')"
94			src="<?php echo esc_url( admin_url( 'admin-ajax.php', 'relative' ) ) . '?action=imgedit-preview&amp;_ajax_nonce=' . $nonce . '&amp;postid=' . $post_id . '&amp;rand=' . rand( 1, 99999 ); ?>" alt="" />
95		</div>
96
97		<div class="imgedit-submit">
98			<input type="button" onclick="imageEdit.close(<?php echo $post_id; ?>, 1)" class="button imgedit-cancel-btn" value="<?php esc_attr_e( 'Cancel' ); ?>" />
99			<input type="button" onclick="imageEdit.save(<?php echo "$post_id, '$nonce'"; ?>)" disabled="disabled" class="button button-primary imgedit-submit-btn" value="<?php esc_attr_e( 'Save' ); ?>" />
100		</div>
101	</div>
102
103	<div class="imgedit-settings">
104	<div class="imgedit-group">
105	<div class="imgedit-group-top">
106		<h2><?php _e( 'Scale Image' ); ?></h2>
107		<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);" aria-expanded="false"><span class="screen-reader-text"><?php esc_html_e( 'Scale Image Help' ); ?></span></button>
108		<div class="imgedit-help">
109		<p><?php _e( 'You can proportionally scale the original image. For best results, scaling should be done before you crop, flip, or rotate. Images can only be scaled down, not up.' ); ?></p>
110		</div>
111		<?php if ( isset( $meta['width'], $meta['height'] ) ) : ?>
112		<p>
113			<?php
114			printf(
115				/* translators: %s: Image width and height in pixels. */
116				__( 'Original dimensions %s' ),
117				'<span class="imgedit-original-dimensions">' . $meta['width'] . ' &times; ' . $meta['height'] . '</span>'
118			);
119			?>
120		</p>
121		<?php endif; ?>
122		<div class="imgedit-submit">
123
124		<fieldset class="imgedit-scale">
125		<legend><?php _e( 'New dimensions:' ); ?></legend>
126		<div class="nowrap">
127		<label for="imgedit-scale-width-<?php echo $post_id; ?>" class="screen-reader-text"><?php _e( 'scale width' ); ?></label>
128		<input type="text" id="imgedit-scale-width-<?php echo $post_id; ?>" onkeyup="imageEdit.scaleChanged(<?php echo $post_id; ?>, 1, this)" onblur="imageEdit.scaleChanged(<?php echo $post_id; ?>, 1, this)" value="<?php echo isset( $meta['width'] ) ? $meta['width'] : 0; ?>" />
129		<span class="imgedit-separator" aria-hidden="true">&times;</span>
130		<label for="imgedit-scale-height-<?php echo $post_id; ?>" class="screen-reader-text"><?php _e( 'scale height' ); ?></label>
131		<input type="text" id="imgedit-scale-height-<?php echo $post_id; ?>" onkeyup="imageEdit.scaleChanged(<?php echo $post_id; ?>, 0, this)" onblur="imageEdit.scaleChanged(<?php echo $post_id; ?>, 0, this)" value="<?php echo isset( $meta['height'] ) ? $meta['height'] : 0; ?>" />
132		<span class="imgedit-scale-warn" id="imgedit-scale-warn-<?php echo $post_id; ?>">!</span>
133		<div class="imgedit-scale-button-wrapper"><input id="imgedit-scale-button" type="button" onclick="imageEdit.action(<?php echo "$post_id, '$nonce'"; ?>, 'scale')" class="button button-primary" value="<?php esc_attr_e( 'Scale' ); ?>" /></div>
134		</div>
135		</fieldset>
136
137		</div>
138	</div>
139	</div>
140
141	<?php if ( $can_restore ) { ?>
142
143	<div class="imgedit-group">
144	<div class="imgedit-group-top">
145		<h2><button type="button" onclick="imageEdit.toggleHelp(this);" class="button-link" aria-expanded="false"><?php _e( 'Restore original image' ); ?> <span class="dashicons dashicons-arrow-down imgedit-help-toggle"></span></button></h2>
146		<div class="imgedit-help imgedit-restore">
147		<p>
148			<?php
149			_e( 'Discard any changes and restore the original image.' );
150
151			if ( ! defined( 'IMAGE_EDIT_OVERWRITE' ) || ! IMAGE_EDIT_OVERWRITE ) {
152				echo ' ' . __( 'Previously edited copies of the image will not be deleted.' );
153			}
154			?>
155		</p>
156		<div class="imgedit-submit">
157		<input type="button" onclick="imageEdit.action(<?php echo "$post_id, '$nonce'"; ?>, 'restore')" class="button button-primary" value="<?php esc_attr_e( 'Restore image' ); ?>" <?php echo $can_restore; ?> />
158		</div>
159		</div>
160	</div>
161	</div>
162
163	<?php } ?>
164
165	<div class="imgedit-group">
166	<div class="imgedit-group-top">
167		<h2><?php _e( 'Image Crop' ); ?></h2>
168		<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);" aria-expanded="false"><span class="screen-reader-text"><?php esc_html_e( 'Image Crop Help' ); ?></span></button>
169
170		<div class="imgedit-help">
171		<p><?php _e( 'To crop the image, click on it and drag to make your selection.' ); ?></p>
172
173		<p><strong><?php _e( 'Crop Aspect Ratio' ); ?></strong><br />
174		<?php _e( 'The aspect ratio is the relationship between the width and height. You can preserve the aspect ratio by holding down the shift key while resizing your selection. Use the input box to specify the aspect ratio, e.g. 1:1 (square), 4:3, 16:9, etc.' ); ?></p>
175
176		<p><strong><?php _e( 'Crop Selection' ); ?></strong><br />
177		<?php _e( 'Once you have made your selection, you can adjust it by entering the size in pixels. The minimum selection size is the thumbnail size as set in the Media settings.' ); ?></p>
178		</div>
179	</div>
180
181	<fieldset class="imgedit-crop-ratio">
182		<legend><?php _e( 'Aspect ratio:' ); ?></legend>
183		<div class="nowrap">
184		<label for="imgedit-crop-width-<?php echo $post_id; ?>" class="screen-reader-text"><?php _e( 'crop ratio width' ); ?></label>
185		<input type="text" id="imgedit-crop-width-<?php echo $post_id; ?>" onkeyup="imageEdit.setRatioSelection(<?php echo $post_id; ?>, 0, this)" onblur="imageEdit.setRatioSelection(<?php echo $post_id; ?>, 0, this)" />
186		<span class="imgedit-separator" aria-hidden="true">:</span>
187		<label for="imgedit-crop-height-<?php echo $post_id; ?>" class="screen-reader-text"><?php _e( 'crop ratio height' ); ?></label>
188		<input type="text" id="imgedit-crop-height-<?php echo $post_id; ?>" onkeyup="imageEdit.setRatioSelection(<?php echo $post_id; ?>, 1, this)" onblur="imageEdit.setRatioSelection(<?php echo $post_id; ?>, 1, this)" />
189		</div>
190	</fieldset>
191
192	<fieldset id="imgedit-crop-sel-<?php echo $post_id; ?>" class="imgedit-crop-sel">
193		<legend><?php _e( 'Selection:' ); ?></legend>
194		<div class="nowrap">
195		<label for="imgedit-sel-width-<?php echo $post_id; ?>" class="screen-reader-text"><?php _e( 'selection width' ); ?></label>
196		<input type="text" id="imgedit-sel-width-<?php echo $post_id; ?>" onkeyup="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" onblur="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" />
197		<span class="imgedit-separator" aria-hidden="true">&times;</span>
198		<label for="imgedit-sel-height-<?php echo $post_id; ?>" class="screen-reader-text"><?php _e( 'selection height' ); ?></label>
199		<input type="text" id="imgedit-sel-height-<?php echo $post_id; ?>" onkeyup="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" onblur="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" />
200		</div>
201	</fieldset>
202
203	</div>
204
205	<?php
206	if ( $thumb && $sub_sizes ) {
207		$thumb_img = wp_constrain_dimensions( $thumb['width'], $thumb['height'], 160, 120 );
208		?>
209
210	<div class="imgedit-group imgedit-applyto">
211	<div class="imgedit-group-top">
212		<h2><?php _e( 'Thumbnail Settings' ); ?></h2>
213		<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);" aria-expanded="false"><span class="screen-reader-text"><?php esc_html_e( 'Thumbnail Settings Help' ); ?></span></button>
214		<div class="imgedit-help">
215		<p><?php _e( 'You can edit the image while preserving the thumbnail. For example, you may wish to have a square thumbnail that displays just a section of the image.' ); ?></p>
216		</div>
217	</div>
218
219	<figure class="imgedit-thumbnail-preview">
220		<img src="<?php echo $thumb['url']; ?>" width="<?php echo $thumb_img[0]; ?>" height="<?php echo $thumb_img[1]; ?>" class="imgedit-size-preview" alt="" draggable="false" />
221		<figcaption class="imgedit-thumbnail-preview-caption"><?php _e( 'Current thumbnail' ); ?></figcaption>
222	</figure>
223
224	<div id="imgedit-save-target-<?php echo $post_id; ?>" class="imgedit-save-target">
225	<fieldset>
226		<legend><?php _e( 'Apply changes to:' ); ?></legend>
227
228		<span class="imgedit-label">
229			<input type="radio" id="imgedit-target-all" name="imgedit-target-<?php echo $post_id; ?>" value="all" checked="checked" />
230			<label for="imgedit-target-all"><?php _e( 'All image sizes' ); ?></label>
231		</span>
232
233		<span class="imgedit-label">
234			<input type="radio" id="imgedit-target-thumbnail" name="imgedit-target-<?php echo $post_id; ?>" value="thumbnail" />
235			<label for="imgedit-target-thumbnail"><?php _e( 'Thumbnail' ); ?></label>
236		</span>
237
238		<span class="imgedit-label">
239			<input type="radio" id="imgedit-target-nothumb" name="imgedit-target-<?php echo $post_id; ?>" value="nothumb" />
240			<label for="imgedit-target-nothumb"><?php _e( 'All sizes except thumbnail' ); ?></label>
241		</span>
242	</fieldset>
243	</div>
244	</div>
245
246	<?php } ?>
247
248	</div>
249
250	</div>
251	<div class="imgedit-wait" id="imgedit-wait-<?php echo $post_id; ?>"></div>
252	<div class="hidden" id="imgedit-leaving-<?php echo $post_id; ?>"><?php _e( "There are unsaved changes that will be lost. 'OK' to continue, 'Cancel' to return to the Image Editor." ); ?></div>
253	</div>
254	<?php
255}
256
257/**
258 * Streams image in WP_Image_Editor to browser.
259 *
260 * @since 2.9.0
261 *
262 * @param WP_Image_Editor $image         The image editor instance.
263 * @param string          $mime_type     The mime type of the image.
264 * @param int             $attachment_id The image's attachment post ID.
265 * @return bool True on success, false on failure.
266 */
267function wp_stream_image( $image, $mime_type, $attachment_id ) {
268	if ( $image instanceof WP_Image_Editor ) {
269
270		/**
271		 * Filters the WP_Image_Editor instance for the image to be streamed to the browser.
272		 *
273		 * @since 3.5.0
274		 *
275		 * @param WP_Image_Editor $image         The image editor instance.
276		 * @param int             $attachment_id The attachment post ID.
277		 */
278		$image = apply_filters( 'image_editor_save_pre', $image, $attachment_id );
279
280		if ( is_wp_error( $image->stream( $mime_type ) ) ) {
281			return false;
282		}
283
284		return true;
285	} else {
286		/* translators: 1: $image, 2: WP_Image_Editor */
287		_deprecated_argument( __FUNCTION__, '3.5.0', sprintf( __( '%1$s needs to be a %2$s object.' ), '$image', 'WP_Image_Editor' ) );
288
289		/**
290		 * Filters the GD image resource to be streamed to the browser.
291		 *
292		 * @since 2.9.0
293		 * @deprecated 3.5.0 Use {@see 'image_editor_save_pre'} instead.
294		 *
295		 * @param resource|GdImage $image         Image resource to be streamed.
296		 * @param int              $attachment_id The attachment post ID.
297		 */
298		$image = apply_filters_deprecated( 'image_save_pre', array( $image, $attachment_id ), '3.5.0', 'image_editor_save_pre' );
299
300		switch ( $mime_type ) {
301			case 'image/jpeg':
302				header( 'Content-Type: image/jpeg' );
303				return imagejpeg( $image, null, 90 );
304			case 'image/png':
305				header( 'Content-Type: image/png' );
306				return imagepng( $image );
307			case 'image/gif':
308				header( 'Content-Type: image/gif' );
309				return imagegif( $image );
310			case 'image/webp':
311				if ( function_exists( 'imagewebp' ) ) {
312					header( 'Content-Type: image/webp' );
313					return imagewebp( $image, null, 90 );
314				}
315				return false;
316			default:
317				return false;
318		}
319	}
320}
321
322/**
323 * Saves image to file.
324 *
325 * @since 2.9.0
326 *
327 * @param string          $filename  Name of the file to be saved.
328 * @param WP_Image_Editor $image     The image editor instance.
329 * @param string          $mime_type The mime type of the image.
330 * @param int             $post_id   Attachment post ID.
331 * @return bool True on success, false on failure.
332 */
333function wp_save_image_file( $filename, $image, $mime_type, $post_id ) {
334	if ( $image instanceof WP_Image_Editor ) {
335
336		/** This filter is documented in wp-admin/includes/image-edit.php */
337		$image = apply_filters( 'image_editor_save_pre', $image, $post_id );
338
339		/**
340		 * Filters whether to skip saving the image file.
341		 *
342		 * Returning a non-null value will short-circuit the save method,
343		 * returning that value instead.
344		 *
345		 * @since 3.5.0
346		 *
347		 * @param bool|null       $override  Value to return instead of saving. Default null.
348		 * @param string          $filename  Name of the file to be saved.
349		 * @param WP_Image_Editor $image     The image editor instance.
350		 * @param string          $mime_type The mime type of the image.
351		 * @param int             $post_id   Attachment post ID.
352		 */
353		$saved = apply_filters( 'wp_save_image_editor_file', null, $filename, $image, $mime_type, $post_id );
354
355		if ( null !== $saved ) {
356			return $saved;
357		}
358
359		return $image->save( $filename, $mime_type );
360	} else {
361		/* translators: 1: $image, 2: WP_Image_Editor */
362		_deprecated_argument( __FUNCTION__, '3.5.0', sprintf( __( '%1$s needs to be a %2$s object.' ), '$image', 'WP_Image_Editor' ) );
363
364		/** This filter is documented in wp-admin/includes/image-edit.php */
365		$image = apply_filters_deprecated( 'image_save_pre', array( $image, $post_id ), '3.5.0', 'image_editor_save_pre' );
366
367		/**
368		 * Filters whether to skip saving the image file.
369		 *
370		 * Returning a non-null value will short-circuit the save method,
371		 * returning that value instead.
372		 *
373		 * @since 2.9.0
374		 * @deprecated 3.5.0 Use {@see 'wp_save_image_editor_file'} instead.
375		 *
376		 * @param mixed           $override  Value to return instead of saving. Default null.
377		 * @param string          $filename  Name of the file to be saved.
378		 * @param WP_Image_Editor $image     The image editor instance.
379		 * @param string          $mime_type The mime type of the image.
380		 * @param int             $post_id   Attachment post ID.
381		 */
382		$saved = apply_filters_deprecated(
383			'wp_save_image_file',
384			array( null, $filename, $image, $mime_type, $post_id ),
385			'3.5.0',
386			'wp_save_image_editor_file'
387		);
388
389		if ( null !== $saved ) {
390			return $saved;
391		}
392
393		switch ( $mime_type ) {
394			case 'image/jpeg':
395				/** This filter is documented in wp-includes/class-wp-image-editor.php */
396				return imagejpeg( $image, $filename, apply_filters( 'jpeg_quality', 90, 'edit_image' ) );
397			case 'image/png':
398				return imagepng( $image, $filename );
399			case 'image/gif':
400				return imagegif( $image, $filename );
401			case 'image/webp':
402				if ( function_exists( 'imagewebp' ) ) {
403					return imagewebp( $image, $filename );
404				}
405				return false;
406			default:
407				return false;
408		}
409	}
410}
411
412/**
413 * Image preview ratio. Internal use only.
414 *
415 * @since 2.9.0
416 *
417 * @ignore
418 * @param int $w Image width in pixels.
419 * @param int $h Image height in pixels.
420 * @return float|int Image preview ratio.
421 */
422function _image_get_preview_ratio( $w, $h ) {
423	$max = max( $w, $h );
424	return $max > 400 ? ( 400 / $max ) : 1;
425}
426
427/**
428 * Returns an image resource. Internal use only.
429 *
430 * @since 2.9.0
431 * @deprecated 3.5.0 Use WP_Image_Editor::rotate()
432 * @see WP_Image_Editor::rotate()
433 *
434 * @ignore
435 * @param resource|GdImage  $img   Image resource.
436 * @param float|int         $angle Image rotation angle, in degrees.
437 * @return resource|GdImage|false GD image resource or GdImage instance, false otherwise.
438 */
439function _rotate_image_resource( $img, $angle ) {
440	_deprecated_function( __FUNCTION__, '3.5.0', 'WP_Image_Editor::rotate()' );
441
442	if ( function_exists( 'imagerotate' ) ) {
443		$rotated = imagerotate( $img, $angle, 0 );
444
445		if ( is_gd_image( $rotated ) ) {
446			imagedestroy( $img );
447			$img = $rotated;
448		}
449	}
450
451	return $img;
452}
453
454/**
455 * Flips an image resource. Internal use only.
456 *
457 * @since 2.9.0
458 * @deprecated 3.5.0 Use WP_Image_Editor::flip()
459 * @see WP_Image_Editor::flip()
460 *
461 * @ignore
462 * @param resource|GdImage $img  Image resource or GdImage instance.
463 * @param bool             $horz Whether to flip horizontally.
464 * @param bool             $vert Whether to flip vertically.
465 * @return resource|GdImage (maybe) flipped image resource or GdImage instance.
466 */
467function _flip_image_resource( $img, $horz, $vert ) {
468	_deprecated_function( __FUNCTION__, '3.5.0', 'WP_Image_Editor::flip()' );
469
470	$w   = imagesx( $img );
471	$h   = imagesy( $img );
472	$dst = wp_imagecreatetruecolor( $w, $h );
473
474	if ( is_gd_image( $dst ) ) {
475		$sx = $vert ? ( $w - 1 ) : 0;
476		$sy = $horz ? ( $h - 1 ) : 0;
477		$sw = $vert ? -$w : $w;
478		$sh = $horz ? -$h : $h;
479
480		if ( imagecopyresampled( $dst, $img, 0, 0, $sx, $sy, $w, $h, $sw, $sh ) ) {
481			imagedestroy( $img );
482			$img = $dst;
483		}
484	}
485
486	return $img;
487}
488
489/**
490 * Crops an image resource. Internal use only.
491 *
492 * @since 2.9.0
493 *
494 * @ignore
495 * @param resource|GdImage $img Image resource or GdImage instance.
496 * @param float            $x   Source point x-coordinate.
497 * @param float            $y   Source point y-coordinate.
498 * @param float            $w   Source width.
499 * @param float            $h   Source height.
500 * @return resource|GdImage (maybe) cropped image resource or GdImage instance.
501 */
502function _crop_image_resource( $img, $x, $y, $w, $h ) {
503	$dst = wp_imagecreatetruecolor( $w, $h );
504
505	if ( is_gd_image( $dst ) ) {
506		if ( imagecopy( $dst, $img, 0, 0, $x, $y, $w, $h ) ) {
507			imagedestroy( $img );
508			$img = $dst;
509		}
510	}
511
512	return $img;
513}
514
515/**
516 * Performs group of changes on Editor specified.
517 *
518 * @since 2.9.0
519 *
520 * @param WP_Image_Editor $image   WP_Image_Editor instance.
521 * @param array           $changes Array of change operations.
522 * @return WP_Image_Editor WP_Image_Editor instance with changes applied.
523 */
524function image_edit_apply_changes( $image, $changes ) {
525	if ( is_gd_image( $image ) ) {
526		/* translators: 1: $image, 2: WP_Image_Editor */
527		_deprecated_argument( __FUNCTION__, '3.5.0', sprintf( __( '%1$s needs to be a %2$s object.' ), '$image', 'WP_Image_Editor' ) );
528	}
529
530	if ( ! is_array( $changes ) ) {
531		return $image;
532	}
533
534	// Expand change operations.
535	foreach ( $changes as $key => $obj ) {
536		if ( isset( $obj->r ) ) {
537			$obj->type  = 'rotate';
538			$obj->angle = $obj->r;
539			unset( $obj->r );
540		} elseif ( isset( $obj->f ) ) {
541			$obj->type = 'flip';
542			$obj->axis = $obj->f;
543			unset( $obj->f );
544		} elseif ( isset( $obj->c ) ) {
545			$obj->type = 'crop';
546			$obj->sel  = $obj->c;
547			unset( $obj->c );
548		}
549		$changes[ $key ] = $obj;
550	}
551
552	// Combine operations.
553	if ( count( $changes ) > 1 ) {
554		$filtered = array( $changes[0] );
555		for ( $i = 0, $j = 1, $c = count( $changes ); $j < $c; $j++ ) {
556			$combined = false;
557			if ( $filtered[ $i ]->type == $changes[ $j ]->type ) {
558				switch ( $filtered[ $i ]->type ) {
559					case 'rotate':
560						$filtered[ $i ]->angle += $changes[ $j ]->angle;
561						$combined               = true;
562						break;
563					case 'flip':
564						$filtered[ $i ]->axis ^= $changes[ $j ]->axis;
565						$combined              = true;
566						break;
567				}
568			}
569			if ( ! $combined ) {
570				$filtered[ ++$i ] = $changes[ $j ];
571			}
572		}
573		$changes = $filtered;
574		unset( $filtered );
575	}
576
577	// Image resource before applying the changes.
578	if ( $image instanceof WP_Image_Editor ) {
579
580		/**
581		 * Filters the WP_Image_Editor instance before applying changes to the image.
582		 *
583		 * @since 3.5.0
584		 *
585		 * @param WP_Image_Editor $image   WP_Image_Editor instance.
586		 * @param array           $changes Array of change operations.
587		 */
588		$image = apply_filters( 'wp_image_editor_before_change', $image, $changes );
589	} elseif ( is_gd_image( $image ) ) {
590
591		/**
592		 * Filters the GD image resource before applying changes to the image.
593		 *
594		 * @since 2.9.0
595		 * @deprecated 3.5.0 Use {@see 'wp_image_editor_before_change'} instead.
596		 *
597		 * @param resource|GdImage $image   GD image resource or GdImage instance.
598		 * @param array            $changes Array of change operations.
599		 */
600		$image = apply_filters_deprecated( 'image_edit_before_change', array( $image, $changes ), '3.5.0', 'wp_image_editor_before_change' );
601	}
602
603	foreach ( $changes as $operation ) {
604		switch ( $operation->type ) {
605			case 'rotate':
606				if ( 0 != $operation->angle ) {
607					if ( $image instanceof WP_Image_Editor ) {
608						$image->rotate( $operation->angle );
609					} else {
610						$image = _rotate_image_resource( $image, $operation->angle );
611					}
612				}
613				break;
614			case 'flip':
615				if ( 0 != $operation->axis ) {
616					if ( $image instanceof WP_Image_Editor ) {
617						$image->flip( ( $operation->axis & 1 ) != 0, ( $operation->axis & 2 ) != 0 );
618					} else {
619						$image = _flip_image_resource( $image, ( $operation->axis & 1 ) != 0, ( $operation->axis & 2 ) != 0 );
620					}
621				}
622				break;
623			case 'crop':
624				$sel = $operation->sel;
625
626				if ( $image instanceof WP_Image_Editor ) {
627					$size = $image->get_size();
628					$w    = $size['width'];
629					$h    = $size['height'];
630
631					$scale = 1 / _image_get_preview_ratio( $w, $h ); // Discard preview scaling.
632					$image->crop( $sel->x * $scale, $sel->y * $scale, $sel->w * $scale, $sel->h * $scale );
633				} else {
634					$scale = 1 / _image_get_preview_ratio( imagesx( $image ), imagesy( $image ) ); // Discard preview scaling.
635					$image = _crop_image_resource( $image, $sel->x * $scale, $sel->y * $scale, $sel->w * $scale, $sel->h * $scale );
636				}
637				break;
638		}
639	}
640
641	return $image;
642}
643
644
645/**
646 * Streams image in post to browser, along with enqueued changes
647 * in `$_REQUEST['history']`.
648 *
649 * @since 2.9.0
650 *
651 * @param int $post_id Attachment post ID.
652 * @return bool True on success, false on failure.
653 */
654function stream_preview_image( $post_id ) {
655	$post = get_post( $post_id );
656
657	wp_raise_memory_limit( 'admin' );
658
659	$img = wp_get_image_editor( _load_image_to_edit_path( $post_id ) );
660
661	if ( is_wp_error( $img ) ) {
662		return false;
663	}
664
665	$changes = ! empty( $_REQUEST['history'] ) ? json_decode( wp_unslash( $_REQUEST['history'] ) ) : null;
666	if ( $changes ) {
667		$img = image_edit_apply_changes( $img, $changes );
668	}
669
670	// Scale the image.
671	$size = $img->get_size();
672	$w    = $size['width'];
673	$h    = $size['height'];
674
675	$ratio = _image_get_preview_ratio( $w, $h );
676	$w2    = max( 1, $w * $ratio );
677	$h2    = max( 1, $h * $ratio );
678
679	if ( is_wp_error( $img->resize( $w2, $h2 ) ) ) {
680		return false;
681	}
682
683	return wp_stream_image( $img, $post->post_mime_type, $post_id );
684}
685
686/**
687 * Restores the metadata for a given attachment.
688 *
689 * @since 2.9.0
690 *
691 * @param int $post_id Attachment post ID.
692 * @return stdClass Image restoration message object.
693 */
694function wp_restore_image( $post_id ) {
695	$meta             = wp_get_attachment_metadata( $post_id );
696	$file             = get_attached_file( $post_id );
697	$backup_sizes     = get_post_meta( $post_id, '_wp_attachment_backup_sizes', true );
698	$old_backup_sizes = $backup_sizes;
699	$restored         = false;
700	$msg              = new stdClass;
701
702	if ( ! is_array( $backup_sizes ) ) {
703		$msg->error = __( 'Cannot load image metadata.' );
704		return $msg;
705	}
706
707	$parts         = pathinfo( $file );
708	$suffix        = time() . rand( 100, 999 );
709	$default_sizes = get_intermediate_image_sizes();
710
711	if ( isset( $backup_sizes['full-orig'] ) && is_array( $backup_sizes['full-orig'] ) ) {
712		$data = $backup_sizes['full-orig'];
713
714		if ( $parts['basename'] != $data['file'] ) {
715			if ( defined( 'IMAGE_EDIT_OVERWRITE' ) && IMAGE_EDIT_OVERWRITE ) {
716
717				// Delete only if it's an edited image.
718				if ( preg_match( '/-e[0-9]{13}\./', $parts['basename'] ) ) {
719					wp_delete_file( $file );
720				}
721			} elseif ( isset( $meta['width'], $meta['height'] ) ) {
722				$backup_sizes[ "full-$suffix" ] = array(
723					'width'  => $meta['width'],
724					'height' => $meta['height'],
725					'file'   => $parts['basename'],
726				);
727			}
728		}
729
730		$restored_file = path_join( $parts['dirname'], $data['file'] );
731		$restored      = update_attached_file( $post_id, $restored_file );
732
733		$meta['file']   = _wp_relative_upload_path( $restored_file );
734		$meta['width']  = $data['width'];
735		$meta['height'] = $data['height'];
736	}
737
738	foreach ( $default_sizes as $default_size ) {
739		if ( isset( $backup_sizes[ "$default_size-orig" ] ) ) {
740			$data = $backup_sizes[ "$default_size-orig" ];
741			if ( isset( $meta['sizes'][ $default_size ] ) && $meta['sizes'][ $default_size ]['file'] != $data['file'] ) {
742				if ( defined( 'IMAGE_EDIT_OVERWRITE' ) && IMAGE_EDIT_OVERWRITE ) {
743
744					// Delete only if it's an edited image.
745					if ( preg_match( '/-e[0-9]{13}-/', $meta['sizes'][ $default_size ]['file'] ) ) {
746						$delete_file = path_join( $parts['dirname'], $meta['sizes'][ $default_size ]['file'] );
747						wp_delete_file( $delete_file );
748					}
749				} else {
750					$backup_sizes[ "$default_size-{$suffix}" ] = $meta['sizes'][ $default_size ];
751				}
752			}
753
754			$meta['sizes'][ $default_size ] = $data;
755		} else {
756			unset( $meta['sizes'][ $default_size ] );
757		}
758	}
759
760	if ( ! wp_update_attachment_metadata( $post_id, $meta ) ||
761		( $old_backup_sizes !== $backup_sizes && ! update_post_meta( $post_id, '_wp_attachment_backup_sizes', $backup_sizes ) ) ) {
762
763		$msg->error = __( 'Cannot save image metadata.' );
764		return $msg;
765	}
766
767	if ( ! $restored ) {
768		$msg->error = __( 'Image metadata is inconsistent.' );
769	} else {
770		$msg->msg = __( 'Image restored successfully.' );
771	}
772
773	return $msg;
774}
775
776/**
777 * Saves image to post, along with enqueued changes
778 * in `$_REQUEST['history']`.
779 *
780 * @since 2.9.0
781 *
782 * @param int $post_id Attachment post ID.
783 * @return stdClass
784 */
785function wp_save_image( $post_id ) {
786	$_wp_additional_image_sizes = wp_get_additional_image_sizes();
787
788	$return  = new stdClass;
789	$success = false;
790	$delete  = false;
791	$scaled  = false;
792	$nocrop  = false;
793	$post    = get_post( $post_id );
794
795	$img = wp_get_image_editor( _load_image_to_edit_path( $post_id, 'full' ) );
796	if ( is_wp_error( $img ) ) {
797		$return->error = esc_js( __( 'Unable to create new image.' ) );
798		return $return;
799	}
800
801	$fwidth  = ! empty( $_REQUEST['fwidth'] ) ? (int) $_REQUEST['fwidth'] : 0;
802	$fheight = ! empty( $_REQUEST['fheight'] ) ? (int) $_REQUEST['fheight'] : 0;
803	$target  = ! empty( $_REQUEST['target'] ) ? preg_replace( '/[^a-z0-9_-]+/i', '', $_REQUEST['target'] ) : '';
804	$scale   = ! empty( $_REQUEST['do'] ) && 'scale' === $_REQUEST['do'];
805
806	if ( $scale && $fwidth > 0 && $fheight > 0 ) {
807		$size = $img->get_size();
808		$sX   = $size['width'];
809		$sY   = $size['height'];
810
811		// Check if it has roughly the same w / h ratio.
812		$diff = round( $sX / $sY, 2 ) - round( $fwidth / $fheight, 2 );
813		if ( -0.1 < $diff && $diff < 0.1 ) {
814			// Scale the full size image.
815			if ( $img->resize( $fwidth, $fheight ) ) {
816				$scaled = true;
817			}
818		}
819
820		if ( ! $scaled ) {
821			$return->error = esc_js( __( 'Error while saving the scaled image. Please reload the page and try again.' ) );
822			return $return;
823		}
824	} elseif ( ! empty( $_REQUEST['history'] ) ) {
825		$changes = json_decode( wp_unslash( $_REQUEST['history'] ) );
826		if ( $changes ) {
827			$img = image_edit_apply_changes( $img, $changes );
828		}
829	} else {
830		$return->error = esc_js( __( 'Nothing to save, the image has not changed.' ) );
831		return $return;
832	}
833
834	$meta         = wp_get_attachment_metadata( $post_id );
835	$backup_sizes = get_post_meta( $post->ID, '_wp_attachment_backup_sizes', true );
836
837	if ( ! is_array( $meta ) ) {
838		$return->error = esc_js( __( 'Image data does not exist. Please re-upload the image.' ) );
839		return $return;
840	}
841
842	if ( ! is_array( $backup_sizes ) ) {
843		$backup_sizes = array();
844	}
845
846	// Generate new filename.
847	$path = get_attached_file( $post_id );
848
849	$basename = pathinfo( $path, PATHINFO_BASENAME );
850	$dirname  = pathinfo( $path, PATHINFO_DIRNAME );
851	$ext      = pathinfo( $path, PATHINFO_EXTENSION );
852	$filename = pathinfo( $path, PATHINFO_FILENAME );
853	$suffix   = time() . rand( 100, 999 );
854
855	if ( defined( 'IMAGE_EDIT_OVERWRITE' ) && IMAGE_EDIT_OVERWRITE &&
856		isset( $backup_sizes['full-orig'] ) && $backup_sizes['full-orig']['file'] != $basename ) {
857
858		if ( 'thumbnail' === $target ) {
859			$new_path = "{$dirname}/{$filename}-temp.{$ext}";
860		} else {
861			$new_path = $path;
862		}
863	} else {
864		while ( true ) {
865			$filename     = preg_replace( '/-e([0-9]+)$/', '', $filename );
866			$filename    .= "-e{$suffix}";
867			$new_filename = "{$filename}.{$ext}";
868			$new_path     = "{$dirname}/$new_filename";
869			if ( file_exists( $new_path ) ) {
870				$suffix++;
871			} else {
872				break;
873			}
874		}
875	}
876
877	// Save the full-size file, also needed to create sub-sizes.
878	if ( ! wp_save_image_file( $new_path, $img, $post->post_mime_type, $post_id ) ) {
879		$return->error = esc_js( __( 'Unable to save the image.' ) );
880		return $return;
881	}
882
883	if ( 'nothumb' === $target || 'all' === $target || 'full' === $target || $scaled ) {
884		$tag = false;
885		if ( isset( $backup_sizes['full-orig'] ) ) {
886			if ( ( ! defined( 'IMAGE_EDIT_OVERWRITE' ) || ! IMAGE_EDIT_OVERWRITE ) && $backup_sizes['full-orig']['file'] !== $basename ) {
887				$tag = "full-$suffix";
888			}
889		} else {
890			$tag = 'full-orig';
891		}
892
893		if ( $tag ) {
894			$backup_sizes[ $tag ] = array(
895				'width'  => $meta['width'],
896				'height' => $meta['height'],
897				'file'   => $basename,
898			);
899		}
900		$success = ( $path === $new_path ) || update_attached_file( $post_id, $new_path );
901
902		$meta['file'] = _wp_relative_upload_path( $new_path );
903
904		$size           = $img->get_size();
905		$meta['width']  = $size['width'];
906		$meta['height'] = $size['height'];
907
908		if ( $success && ( 'nothumb' === $target || 'all' === $target ) ) {
909			$sizes = get_intermediate_image_sizes();
910			if ( 'nothumb' === $target ) {
911				$sizes = array_diff( $sizes, array( 'thumbnail' ) );
912			}
913		}
914
915		$return->fw = $meta['width'];
916		$return->fh = $meta['height'];
917	} elseif ( 'thumbnail' === $target ) {
918		$sizes   = array( 'thumbnail' );
919		$success = true;
920		$delete  = true;
921		$nocrop  = true;
922	}
923
924	/*
925	 * We need to remove any existing resized image files because
926	 * a new crop or rotate could generate different sizes (and hence, filenames),
927	 * keeping the new resized images from overwriting the existing image files.
928	 * https://core.trac.wordpress.org/ticket/32171
929	 */
930	if ( defined( 'IMAGE_EDIT_OVERWRITE' ) && IMAGE_EDIT_OVERWRITE && ! empty( $meta['sizes'] ) ) {
931		foreach ( $meta['sizes'] as $size ) {
932			if ( ! empty( $size['file'] ) && preg_match( '/-e[0-9]{13}-/', $size['file'] ) ) {
933				$delete_file = path_join( $dirname, $size['file'] );
934				wp_delete_file( $delete_file );
935			}
936		}
937	}
938
939	if ( isset( $sizes ) ) {
940		$_sizes = array();
941
942		foreach ( $sizes as $size ) {
943			$tag = false;
944			if ( isset( $meta['sizes'][ $size ] ) ) {
945				if ( isset( $backup_sizes[ "$size-orig" ] ) ) {
946					if ( ( ! defined( 'IMAGE_EDIT_OVERWRITE' ) || ! IMAGE_EDIT_OVERWRITE ) && $backup_sizes[ "$size-orig" ]['file'] != $meta['sizes'][ $size ]['file'] ) {
947						$tag = "$size-$suffix";
948					}
949				} else {
950					$tag = "$size-orig";
951				}
952
953				if ( $tag ) {
954					$backup_sizes[ $tag ] = $meta['sizes'][ $size ];
955				}
956			}
957
958			if ( isset( $_wp_additional_image_sizes[ $size ] ) ) {
959				$width  = (int) $_wp_additional_image_sizes[ $size ]['width'];
960				$height = (int) $_wp_additional_image_sizes[ $size ]['height'];
961				$crop   = ( $nocrop ) ? false : $_wp_additional_image_sizes[ $size ]['crop'];
962			} else {
963				$height = get_option( "{$size}_size_h" );
964				$width  = get_option( "{$size}_size_w" );
965				$crop   = ( $nocrop ) ? false : get_option( "{$size}_crop" );
966			}
967
968			$_sizes[ $size ] = array(
969				'width'  => $width,
970				'height' => $height,
971				'crop'   => $crop,
972			);
973		}
974
975		$meta['sizes'] = array_merge( $meta['sizes'], $img->multi_resize( $_sizes ) );
976	}
977
978	unset( $img );
979
980	if ( $success ) {
981		wp_update_attachment_metadata( $post_id, $meta );
982		update_post_meta( $post_id, '_wp_attachment_backup_sizes', $backup_sizes );
983
984		if ( 'thumbnail' === $target || 'all' === $target || 'full' === $target ) {
985			// Check if it's an image edit from attachment edit screen.
986			if ( ! empty( $_REQUEST['context'] ) && 'edit-attachment' === $_REQUEST['context'] ) {
987				$thumb_url         = wp_get_attachment_image_src( $post_id, array( 900, 600 ), true );
988				$return->thumbnail = $thumb_url[0];
989			} else {
990				$file_url = wp_get_attachment_url( $post_id );
991				if ( ! empty( $meta['sizes']['thumbnail'] ) ) {
992					$thumb             = $meta['sizes']['thumbnail'];
993					$return->thumbnail = path_join( dirname( $file_url ), $thumb['file'] );
994				} else {
995					$return->thumbnail = "$file_url?w=128&h=128";
996				}
997			}
998		}
999	} else {
1000		$delete = true;
1001	}
1002
1003	if ( $delete ) {
1004		wp_delete_file( $new_path );
1005	}
1006
1007	$return->msg = esc_js( __( 'Image saved' ) );
1008	return $return;
1009}
1010