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