WordPress 3.8
[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 function wp_image_editor($post_id, $msg = false) {
10         $nonce = wp_create_nonce("image_editor-$post_id");
11         $meta = wp_get_attachment_metadata($post_id);
12         $thumb = image_get_intermediate_size($post_id, 'thumbnail');
13         $sub_sizes = isset($meta['sizes']) && is_array($meta['sizes']);
14         $note = '';
15
16         if ( isset( $meta['width'], $meta['height'] ) )
17                 $big = max( $meta['width'], $meta['height'] );
18         else
19                 die( __('Image data does not exist. Please re-upload the image.') );
20
21         $sizer = $big > 400 ? 400 / $big : 1;
22
23         $backup_sizes = get_post_meta( $post_id, '_wp_attachment_backup_sizes', true );
24         $can_restore = false;
25         if ( ! empty( $backup_sizes ) && isset( $backup_sizes['full-orig'], $meta['file'] ) )
26                 $can_restore = $backup_sizes['full-orig']['file'] != basename( $meta['file'] );
27
28         if ( $msg ) {
29                 if ( isset($msg->error) )
30                         $note = "<div class='error'><p>$msg->error</p></div>";
31                 elseif ( isset($msg->msg) )
32                         $note = "<div class='updated'><p>$msg->msg</p></div>";
33         }
34
35         ?>
36         <div class="imgedit-wrap">
37         <?php echo $note; ?>
38         <table id="imgedit-panel-<?php echo $post_id; ?>"><tbody>
39         <tr><td>
40         <div class="imgedit-menu">
41                 <div onclick="imageEdit.crop(<?php echo "$post_id, '$nonce'"; ?>, this)" class="imgedit-crop disabled" title="<?php esc_attr_e( 'Crop' ); ?>"></div><?php
42
43         // On some setups GD library does not provide imagerotate() - Ticket #11536
44         if ( wp_image_editor_supports( array( 'mime_type' => get_post_mime_type( $post_id ), 'methods' => array( 'rotate' ) ) ) ) { ?>
45                 <div class="imgedit-rleft"  onclick="imageEdit.rotate( 90, <?php echo "$post_id, '$nonce'"; ?>, this)" title="<?php esc_attr_e( 'Rotate counter-clockwise' ); ?>"></div>
46                 <div class="imgedit-rright" onclick="imageEdit.rotate(-90, <?php echo "$post_id, '$nonce'"; ?>, this)" title="<?php esc_attr_e( 'Rotate clockwise' ); ?>"></div>
47 <?php } else {
48                 $note_no_rotate = esc_attr__('Image rotation is not supported by your web host.');
49 ?>
50             <div class="imgedit-rleft disabled"  title="<?php echo $note_no_rotate; ?>"></div>
51             <div class="imgedit-rright disabled" title="<?php echo $note_no_rotate; ?>"></div>
52 <?php } ?>
53
54                 <div onclick="imageEdit.flip(1, <?php echo "$post_id, '$nonce'"; ?>, this)" class="imgedit-flipv" title="<?php esc_attr_e( 'Flip vertically' ); ?>"></div>
55                 <div onclick="imageEdit.flip(2, <?php echo "$post_id, '$nonce'"; ?>, this)" class="imgedit-fliph" title="<?php esc_attr_e( 'Flip horizontally' ); ?>"></div>
56
57                 <div id="image-undo-<?php echo $post_id; ?>" onclick="imageEdit.undo(<?php echo "$post_id, '$nonce'"; ?>, this)" class="imgedit-undo disabled" title="<?php esc_attr_e( 'Undo' ); ?>"></div>
58                 <div id="image-redo-<?php echo $post_id; ?>" onclick="imageEdit.redo(<?php echo "$post_id, '$nonce'"; ?>, this)" class="imgedit-redo disabled" title="<?php esc_attr_e( 'Redo' ); ?>"></div>
59                 <br class="clear" />
60         </div>
61
62         <input type="hidden" id="imgedit-sizer-<?php echo $post_id; ?>" value="<?php echo $sizer; ?>" />
63         <input type="hidden" id="imgedit-minthumb-<?php echo $post_id; ?>" value="<?php echo ( get_option('thumbnail_size_w') . ':' . get_option('thumbnail_size_h') ); ?>" />
64         <input type="hidden" id="imgedit-history-<?php echo $post_id; ?>" value="" />
65         <input type="hidden" id="imgedit-undone-<?php echo $post_id; ?>" value="0" />
66         <input type="hidden" id="imgedit-selection-<?php echo $post_id; ?>" value="" />
67         <input type="hidden" id="imgedit-x-<?php echo $post_id; ?>" value="<?php echo isset( $meta['width'] ) ? $meta['width'] : 0; ?>" />
68         <input type="hidden" id="imgedit-y-<?php echo $post_id; ?>" value="<?php echo isset( $meta['height'] ) ? $meta['height'] : 0; ?>" />
69
70         <div id="imgedit-crop-<?php echo $post_id; ?>" class="imgedit-crop-wrap">
71         <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); ?>" />
72         </div>
73
74         <div class="imgedit-submit">
75                 <input type="button" onclick="imageEdit.close(<?php echo $post_id; ?>, 1)" class="button" value="<?php esc_attr_e( 'Cancel' ); ?>" />
76                 <input type="button" onclick="imageEdit.save(<?php echo "$post_id, '$nonce'"; ?>)" disabled="disabled" class="button-primary imgedit-submit-btn" value="<?php esc_attr_e( 'Save' ); ?>" />
77         </div>
78         </td>
79
80         <td class="imgedit-settings">
81         <div class="imgedit-group">
82         <div class="imgedit-group-top">
83                 <a class="imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);return false;" href="#"><strong><?php _e('Scale Image'); ?></strong></a>
84                 <div class="imgedit-help">
85                 <p><?php _e('You can proportionally scale the original image. For best results the scaling should be done before performing any other operations on it like crop, rotate, etc. Note that images can only be scaled down, not up.'); ?></p>
86                 <?php if ( isset( $meta['width'], $meta['height'] ) ): ?>
87                 <p><?php printf( __('Original dimensions %s'), $meta['width'] . '&times;' . $meta['height'] ); ?></p>
88                 <?php endif ?>
89                 <div class="imgedit-submit">
90                 <span class="nowrap"><input type="text" id="imgedit-scale-width-<?php echo $post_id; ?>" onkeyup="imageEdit.scaleChanged(<?php echo $post_id; ?>, 1)" onblur="imageEdit.scaleChanged(<?php echo $post_id; ?>, 1)" style="width:4em;" value="<?php echo isset( $meta['width'] ) ? $meta['width'] : 0; ?>" />&times;<input type="text" id="imgedit-scale-height-<?php echo $post_id; ?>" onkeyup="imageEdit.scaleChanged(<?php echo $post_id; ?>, 0)" onblur="imageEdit.scaleChanged(<?php echo $post_id; ?>, 0)" style="width:4em;" value="<?php echo isset( $meta['height'] ) ? $meta['height'] : 0; ?>" />
91                 <span class="imgedit-scale-warn" id="imgedit-scale-warn-<?php echo $post_id; ?>">!</span></span>
92                 <input type="button" onclick="imageEdit.action(<?php echo "$post_id, '$nonce'"; ?>, 'scale')" class="button-primary" value="<?php esc_attr_e( 'Scale' ); ?>" />
93                 </div>
94                 </div>
95         </div>
96
97 <?php if ( $can_restore ) { ?>
98
99         <div class="imgedit-group-top">
100                 <a class="imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);return false;" href="#"><strong><?php _e('Restore Original Image'); ?></strong></a>
101                 <div class="imgedit-help">
102                 <p><?php _e('Discard any changes and restore the original image.');
103
104                 if ( !defined('IMAGE_EDIT_OVERWRITE') || !IMAGE_EDIT_OVERWRITE )
105                         echo ' '.__('Previously edited copies of the image will not be deleted.');
106
107                 ?></p>
108                 <div class="imgedit-submit">
109                 <input type="button" onclick="imageEdit.action(<?php echo "$post_id, '$nonce'"; ?>, 'restore')" class="button-primary" value="<?php esc_attr_e( 'Restore image' ); ?>" <?php echo $can_restore; ?> />
110                 </div>
111                 </div>
112         </div>
113
114 <?php } ?>
115
116         </div>
117
118         <div class="imgedit-group">
119         <div class="imgedit-group-top">
120                 <strong><?php _e('Image Crop'); ?></strong>
121                 <a class="imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);return false;" href="#"><?php _e('(help)'); ?></a>
122                 <div class="imgedit-help">
123                 <p><?php _e('The image can be cropped by clicking on it and dragging to select the desired part. While dragging the dimensions of the selection are displayed below.'); ?></p>
124
125                 <p><strong><?php _e('Crop Aspect Ratio'); ?></strong><br />
126                 <?php _e('You can specify the crop selection aspect ratio then hold down the Shift key while dragging to lock it. The values can be 1:1 (square), 4:3, 16:9, etc. If there is a selection, specifying aspect ratio will set it immediately.'); ?></p>
127
128                 <p><strong><?php _e('Crop Selection'); ?></strong><br />
129                 <?php _e('Once started, the selection can be adjusted by entering new values (in pixels). Note that these values are scaled to approximately match the original image dimensions. The minimum selection size equals the thumbnail size as set in the Media settings.'); ?></p>
130                 </div>
131         </div>
132
133         <p>
134                 <?php _e('Aspect ratio:'); ?>
135                 <span  class="nowrap">
136                 <input type="text" id="imgedit-crop-width-<?php echo $post_id; ?>" onkeyup="imageEdit.setRatioSelection(<?php echo $post_id; ?>, 0, this)" style="width:3em;" />
137                 :
138                 <input type="text" id="imgedit-crop-height-<?php echo $post_id; ?>" onkeyup="imageEdit.setRatioSelection(<?php echo $post_id; ?>, 1, this)" style="width:3em;" />
139                 </span>
140         </p>
141
142         <p id="imgedit-crop-sel-<?php echo $post_id; ?>">
143                 <?php _e('Selection:'); ?>
144                 <span  class="nowrap">
145                 <input type="text" id="imgedit-sel-width-<?php echo $post_id; ?>" onkeyup="imageEdit.setNumSelection(<?php echo $post_id; ?>)" style="width:4em;" />
146                 :
147                 <input type="text" id="imgedit-sel-height-<?php echo $post_id; ?>" onkeyup="imageEdit.setNumSelection(<?php echo $post_id; ?>)" style="width:4em;" />
148                 </span>
149         </p>
150         </div>
151
152         <?php if ( $thumb && $sub_sizes ) {
153                 $thumb_img = wp_constrain_dimensions( $thumb['width'], $thumb['height'], 160, 120 );
154         ?>
155
156         <div class="imgedit-group imgedit-applyto">
157         <div class="imgedit-group-top">
158                 <strong><?php _e('Thumbnail Settings'); ?></strong>
159                 <a class="imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);return false;" href="#"><?php _e('(help)'); ?></a>
160                 <p class="imgedit-help"><?php _e('The thumbnail image can be cropped differently. For example it can be square or contain only a portion of the original image to showcase it better. Here you can select whether to apply changes to all image sizes or make the thumbnail different.'); ?></p>
161         </div>
162
163         <p>
164                 <img src="<?php echo $thumb['url']; ?>" width="<?php echo $thumb_img[0]; ?>" height="<?php echo $thumb_img[1]; ?>" class="imgedit-size-preview" alt="" /><br /><?php _e('Current thumbnail'); ?>
165         </p>
166
167         <p id="imgedit-save-target-<?php echo $post_id; ?>">
168                 <strong><?php _e('Apply changes to:'); ?></strong><br />
169
170                 <label class="imgedit-label">
171                 <input type="radio" name="imgedit-target-<?php echo $post_id; ?>" value="all" checked="checked" />
172                 <?php _e('All image sizes'); ?></label>
173
174                 <label class="imgedit-label">
175                 <input type="radio" name="imgedit-target-<?php echo $post_id; ?>" value="thumbnail" />
176                 <?php _e('Thumbnail'); ?></label>
177
178                 <label class="imgedit-label">
179                 <input type="radio" name="imgedit-target-<?php echo $post_id; ?>" value="nothumb" />
180                 <?php _e('All sizes except thumbnail'); ?></label>
181         </p>
182         </div>
183
184         <?php } ?>
185
186         </td></tr>
187         </tbody></table>
188         <div class="imgedit-wait" id="imgedit-wait-<?php echo $post_id; ?>"></div>
189         <script type="text/javascript">jQuery( function() { imageEdit.init(<?php echo $post_id; ?>); });</script>
190         <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>
191         </div>
192 <?php
193 }
194
195 /**
196  * Streams image in WP_Image_Editor to browser.
197  * Provided for backcompat reasons
198  *
199  * @param WP_Image_Editor $image
200  * @param string $mime_type
201  * @param int $post_id
202  * @return boolean
203  */
204 function wp_stream_image( $image, $mime_type, $post_id ) {
205         if ( $image instanceof WP_Image_Editor ) {
206                 $image = apply_filters('image_editor_save_pre', $image, $post_id);
207
208                 if ( is_wp_error( $image->stream( $mime_type ) ) )
209                         return false;
210
211                 return true;
212         } else {
213                 _deprecated_argument( __FUNCTION__, '3.5', __( '$image needs to be an WP_Image_Editor object' ) );
214
215                 $image = apply_filters('image_save_pre', $image, $post_id);
216
217                 switch ( $mime_type ) {
218                         case 'image/jpeg':
219                                 header( 'Content-Type: image/jpeg' );
220                                 return imagejpeg( $image, null, 90 );
221                         case 'image/png':
222                                 header( 'Content-Type: image/png' );
223                                 return imagepng( $image );
224                         case 'image/gif':
225                                 header( 'Content-Type: image/gif' );
226                                 return imagegif( $image );
227                         default:
228                                 return false;
229                 }
230         }
231 }
232
233 /**
234  * Saves Image to File
235  *
236  * @param string $filename
237  * @param WP_Image_Editor $image
238  * @param string $mime_type
239  * @param int $post_id
240  * @return boolean
241  */
242 function wp_save_image_file( $filename, $image, $mime_type, $post_id ) {
243         if ( $image instanceof WP_Image_Editor ) {
244                 $image = apply_filters('image_editor_save_pre', $image, $post_id);
245                 $saved = apply_filters('wp_save_image_editor_file', null, $filename, $image, $mime_type, $post_id);
246
247                 if ( null !== $saved )
248                         return $saved;
249
250                 return $image->save( $filename, $mime_type );
251         } else {
252                 _deprecated_argument( __FUNCTION__, '3.5', __( '$image needs to be an WP_Image_Editor object' ) );
253
254                 $image = apply_filters('image_save_pre', $image, $post_id);
255                 $saved = apply_filters('wp_save_image_file', null, $filename, $image, $mime_type, $post_id);
256
257                 if ( null !== $saved )
258                         return $saved;
259
260                 switch ( $mime_type ) {
261                         case 'image/jpeg':
262                                 /** This filter is documented in wp-includes/class-wp-image-editor.php */
263                                 return imagejpeg( $image, $filename, apply_filters( 'jpeg_quality', 90, 'edit_image' ) );
264                         case 'image/png':
265                                 return imagepng( $image, $filename );
266                         case 'image/gif':
267                                 return imagegif( $image, $filename );
268                         default:
269                                 return false;
270                 }
271         }
272 }
273
274 function _image_get_preview_ratio($w, $h) {
275         $max = max($w, $h);
276         return $max > 400 ? (400 / $max) : 1;
277 }
278
279 // @TODO: Returns GD resource, but is NOT public
280 function _rotate_image_resource($img, $angle) {
281         _deprecated_function( __FUNCTION__, '3.5', __( 'Use WP_Image_Editor::rotate' ) );
282         if ( function_exists('imagerotate') ) {
283                 $rotated = imagerotate($img, $angle, 0);
284                 if ( is_resource($rotated) ) {
285                         imagedestroy($img);
286                         $img = $rotated;
287                 }
288         }
289         return $img;
290 }
291
292 /**
293  * @TODO: Only used within image_edit_apply_changes
294  *                and receives/returns GD Resource.
295  *                Consider removal.
296  *
297  * @param GD_Resource $img
298  * @param boolean $horz
299  * @param boolean $vert
300  * @return GD_Resource
301  */
302 function _flip_image_resource($img, $horz, $vert) {
303         _deprecated_function( __FUNCTION__, '3.5', __( 'Use WP_Image_Editor::flip' ) );
304         $w = imagesx($img);
305         $h = imagesy($img);
306         $dst = wp_imagecreatetruecolor($w, $h);
307         if ( is_resource($dst) ) {
308                 $sx = $vert ? ($w - 1) : 0;
309                 $sy = $horz ? ($h - 1) : 0;
310                 $sw = $vert ? -$w : $w;
311                 $sh = $horz ? -$h : $h;
312
313                 if ( imagecopyresampled($dst, $img, 0, 0, $sx, $sy, $w, $h, $sw, $sh) ) {
314                         imagedestroy($img);
315                         $img = $dst;
316                 }
317         }
318         return $img;
319 }
320
321 /**
322  * @TODO: Only used within image_edit_apply_changes
323  *                and receives/returns GD Resource.
324  *                Consider removal.
325  *
326  * @param GD_Resource $img
327  * @param float $x
328  * @param float $y
329  * @param float $w
330  * @param float $h
331  * @return GD_Resource
332  */
333 function _crop_image_resource($img, $x, $y, $w, $h) {
334         $dst = wp_imagecreatetruecolor($w, $h);
335         if ( is_resource($dst) ) {
336                 if ( imagecopy($dst, $img, 0, 0, $x, $y, $w, $h) ) {
337                         imagedestroy($img);
338                         $img = $dst;
339                 }
340         }
341         return $img;
342 }
343
344 /**
345  * Performs group of changes on Editor specified.
346  *
347  * @param WP_Image_Editor $image
348  * @param type $changes
349  * @return WP_Image_Editor
350  */
351 function image_edit_apply_changes( $image, $changes ) {
352         if ( is_resource( $image ) )
353                 _deprecated_argument( __FUNCTION__, '3.5', __( '$image needs to be an WP_Image_Editor object' ) );
354
355         if ( !is_array($changes) )
356                 return $image;
357
358         // expand change operations
359         foreach ( $changes as $key => $obj ) {
360                 if ( isset($obj->r) ) {
361                         $obj->type = 'rotate';
362                         $obj->angle = $obj->r;
363                         unset($obj->r);
364                 } elseif ( isset($obj->f) ) {
365                         $obj->type = 'flip';
366                         $obj->axis = $obj->f;
367                         unset($obj->f);
368                 } elseif ( isset($obj->c) ) {
369                         $obj->type = 'crop';
370                         $obj->sel = $obj->c;
371                         unset($obj->c);
372                 }
373                 $changes[$key] = $obj;
374         }
375
376         // combine operations
377         if ( count($changes) > 1 ) {
378                 $filtered = array($changes[0]);
379                 for ( $i = 0, $j = 1; $j < count($changes); $j++ ) {
380                         $combined = false;
381                         if ( $filtered[$i]->type == $changes[$j]->type ) {
382                                 switch ( $filtered[$i]->type ) {
383                                         case 'rotate':
384                                                 $filtered[$i]->angle += $changes[$j]->angle;
385                                                 $combined = true;
386                                                 break;
387                                         case 'flip':
388                                                 $filtered[$i]->axis ^= $changes[$j]->axis;
389                                                 $combined = true;
390                                                 break;
391                                 }
392                         }
393                         if ( !$combined )
394                                 $filtered[++$i] = $changes[$j];
395                 }
396                 $changes = $filtered;
397                 unset($filtered);
398         }
399
400         // image resource before applying the changes
401         if ( $image instanceof WP_Image_Editor )
402                 $image = apply_filters('wp_image_editor_before_change', $image, $changes);
403         elseif ( is_resource( $image ) )
404                 $image = apply_filters('image_edit_before_change', $image, $changes);
405
406         foreach ( $changes as $operation ) {
407                 switch ( $operation->type ) {
408                         case 'rotate':
409                                 if ( $operation->angle != 0 ) {
410                                         if ( $image instanceof WP_Image_Editor )
411                                                 $image->rotate( $operation->angle );
412                                         else
413                                                 $image = _rotate_image_resource( $image, $operation->angle );
414                                 }
415                                 break;
416                         case 'flip':
417                                 if ( $operation->axis != 0 )
418                                         if ( $image instanceof WP_Image_Editor )
419                                                 $image->flip( ($operation->axis & 1) != 0, ($operation->axis & 2) != 0 );
420                                         else
421                                                 $image = _flip_image_resource( $image, ( $operation->axis & 1 ) != 0, ( $operation->axis & 2 ) != 0 );
422                                 break;
423                         case 'crop':
424                                 $sel = $operation->sel;
425
426                                 if ( $image instanceof WP_Image_Editor ) {
427                                         $size = $image->get_size();
428                                         $w = $size['width'];
429                                         $h = $size['height'];
430
431                                         $scale = 1 / _image_get_preview_ratio( $w, $h ); // discard preview scaling
432                                         $image->crop( $sel->x * $scale, $sel->y * $scale, $sel->w * $scale, $sel->h * $scale );
433                                 } else {
434                                         $scale = 1 / _image_get_preview_ratio( imagesx( $image ), imagesy( $image ) ); // discard preview scaling
435                                         $image = _crop_image_resource( $image, $sel->x * $scale, $sel->y * $scale, $sel->w * $scale, $sel->h * $scale );
436                                 }
437                                 break;
438                 }
439         }
440
441         return $image;
442 }
443
444
445 /**
446  * Streams image in post to browser, along with enqueued changes
447  * in $_REQUEST['history']
448  *
449  * @param int $post_id
450  * @return boolean
451  */
452 function stream_preview_image( $post_id ) {
453         $post = get_post( $post_id );
454         @ini_set( 'memory_limit', apply_filters( 'admin_memory_limit', WP_MAX_MEMORY_LIMIT ) );
455
456         $img = wp_get_image_editor( _load_image_to_edit_path( $post_id ) );
457
458     if ( is_wp_error( $img ) )
459         return false;
460
461         $changes = !empty($_REQUEST['history']) ? json_decode( wp_unslash($_REQUEST['history']) ) : null;
462         if ( $changes )
463                 $img = image_edit_apply_changes( $img, $changes );
464
465         // scale the image
466         $size = $img->get_size();
467         $w = $size['width'];
468         $h = $size['height'];
469
470         $ratio = _image_get_preview_ratio( $w, $h );
471         $w2 = max ( 1, $w * $ratio );
472         $h2 = max ( 1, $h * $ratio );
473
474         if ( is_wp_error( $img->resize( $w2, $h2 ) ) )
475                 return false;
476
477         return wp_stream_image( $img, $post->post_mime_type, $post_id );
478 }
479
480 function wp_restore_image($post_id) {
481         $meta = wp_get_attachment_metadata($post_id);
482         $file = get_attached_file($post_id);
483         $backup_sizes = get_post_meta( $post_id, '_wp_attachment_backup_sizes', true );
484         $restored = false;
485         $msg = new stdClass;
486
487         if ( !is_array($backup_sizes) ) {
488                 $msg->error = __('Cannot load image metadata.');
489                 return $msg;
490         }
491
492         $parts = pathinfo($file);
493         $suffix = time() . rand(100, 999);
494         $default_sizes = get_intermediate_image_sizes();
495
496         if ( isset($backup_sizes['full-orig']) && is_array($backup_sizes['full-orig']) ) {
497                 $data = $backup_sizes['full-orig'];
498
499                 if ( $parts['basename'] != $data['file'] ) {
500                         if ( defined('IMAGE_EDIT_OVERWRITE') && IMAGE_EDIT_OVERWRITE ) {
501                                 // delete only if it's edited image
502                                 if ( preg_match('/-e[0-9]{13}\./', $parts['basename']) ) {
503                                         /** This filter is documented in wp-admin/custom-header.php */
504                                         $delpath = apply_filters('wp_delete_file', $file);
505                                         @unlink($delpath);
506                                 }
507                         } elseif ( isset( $meta['width'], $meta['height'] ) ) {
508                                 $backup_sizes["full-$suffix"] = array('width' => $meta['width'], 'height' => $meta['height'], 'file' => $parts['basename']);
509                         }
510                 }
511
512                 $restored_file = path_join($parts['dirname'], $data['file']);
513                 $restored = update_attached_file($post_id, $restored_file);
514
515                 $meta['file'] = _wp_relative_upload_path( $restored_file );
516                 $meta['width'] = $data['width'];
517                 $meta['height'] = $data['height'];
518         }
519
520         foreach ( $default_sizes as $default_size ) {
521                 if ( isset($backup_sizes["$default_size-orig"]) ) {
522                         $data = $backup_sizes["$default_size-orig"];
523                         if ( isset($meta['sizes'][$default_size]) && $meta['sizes'][$default_size]['file'] != $data['file'] ) {
524                                 if ( defined('IMAGE_EDIT_OVERWRITE') && IMAGE_EDIT_OVERWRITE ) {
525                                         // delete only if it's edited image
526                                         if ( preg_match('/-e[0-9]{13}-/', $meta['sizes'][$default_size]['file']) ) {
527                                                 /** This filter is documented in wp-admin/custom-header.php */
528                                                 $delpath = apply_filters( 'wp_delete_file', path_join($parts['dirname'], $meta['sizes'][$default_size]['file']) );
529                                                 @unlink($delpath);
530                                         }
531                                 } else {
532                                         $backup_sizes["$default_size-{$suffix}"] = $meta['sizes'][$default_size];
533                                 }
534                         }
535
536                         $meta['sizes'][$default_size] = $data;
537                 } else {
538                         unset($meta['sizes'][$default_size]);
539                 }
540         }
541
542         if ( !wp_update_attachment_metadata($post_id, $meta) || !update_post_meta( $post_id, '_wp_attachment_backup_sizes', $backup_sizes) ) {
543                 $msg->error = __('Cannot save image metadata.');
544                 return $msg;
545         }
546
547         if ( !$restored )
548                 $msg->error = __('Image metadata is inconsistent.');
549         else
550                 $msg->msg = __('Image restored successfully.');
551
552         return $msg;
553 }
554
555 /**
556  * Saves image to post along with enqueued changes
557  * in $_REQUEST['history']
558  *
559  * @param int $post_id
560  * @return \stdClass
561  */
562 function wp_save_image( $post_id ) {
563         $return = new stdClass;
564         $success = $delete = $scaled = $nocrop = false;
565         $post = get_post( $post_id );
566
567         $img = wp_get_image_editor( _load_image_to_edit_path( $post_id, 'full' ) );
568         if ( is_wp_error( $img ) ) {
569                 $return->error = esc_js( __('Unable to create new image.') );
570                 return $return;
571         }
572
573         $fwidth = !empty($_REQUEST['fwidth']) ? intval($_REQUEST['fwidth']) : 0;
574         $fheight = !empty($_REQUEST['fheight']) ? intval($_REQUEST['fheight']) : 0;
575         $target = !empty($_REQUEST['target']) ? preg_replace('/[^a-z0-9_-]+/i', '', $_REQUEST['target']) : '';
576         $scale = !empty($_REQUEST['do']) && 'scale' == $_REQUEST['do'];
577
578         if ( $scale && $fwidth > 0 && $fheight > 0 ) {
579                 $size = $img->get_size();
580                 $sX = $size['width'];
581                 $sY = $size['height'];
582
583                 // check if it has roughly the same w / h ratio
584                 $diff = round($sX / $sY, 2) - round($fwidth / $fheight, 2);
585                 if ( -0.1 < $diff && $diff < 0.1 ) {
586                         // scale the full size image
587                         if ( $img->resize( $fwidth, $fheight ) )
588                                 $scaled = true;
589                 }
590
591                 if ( !$scaled ) {
592                         $return->error = esc_js( __('Error while saving the scaled image. Please reload the page and try again.') );
593                         return $return;
594                 }
595         } elseif ( !empty($_REQUEST['history']) ) {
596                 $changes = json_decode( wp_unslash($_REQUEST['history']) );
597                 if ( $changes )
598                         $img = image_edit_apply_changes($img, $changes);
599         } else {
600                 $return->error = esc_js( __('Nothing to save, the image has not changed.') );
601                 return $return;
602         }
603
604         $meta = wp_get_attachment_metadata($post_id);
605         $backup_sizes = get_post_meta( $post->ID, '_wp_attachment_backup_sizes', true );
606
607         if ( !is_array($meta) ) {
608                 $return->error = esc_js( __('Image data does not exist. Please re-upload the image.') );
609                 return $return;
610         }
611
612         if ( !is_array($backup_sizes) )
613                 $backup_sizes = array();
614
615         // generate new filename
616         $path = get_attached_file($post_id);
617         $path_parts = pathinfo( $path );
618         $filename = $path_parts['filename'];
619         $suffix = time() . rand(100, 999);
620
621         if ( defined('IMAGE_EDIT_OVERWRITE') && IMAGE_EDIT_OVERWRITE &&
622                 isset($backup_sizes['full-orig']) && $backup_sizes['full-orig']['file'] != $path_parts['basename'] ) {
623
624                 if ( 'thumbnail' == $target )
625                         $new_path = "{$path_parts['dirname']}/{$filename}-temp.{$path_parts['extension']}";
626                 else
627                         $new_path = $path;
628         } else {
629                 while( true ) {
630                         $filename = preg_replace( '/-e([0-9]+)$/', '', $filename );
631                         $filename .= "-e{$suffix}";
632                         $new_filename = "{$filename}.{$path_parts['extension']}";
633                         $new_path = "{$path_parts['dirname']}/$new_filename";
634                         if ( file_exists($new_path) )
635                                 $suffix++;
636                         else
637                                 break;
638                 }
639         }
640
641         // save the full-size file, also needed to create sub-sizes
642         if ( !wp_save_image_file($new_path, $img, $post->post_mime_type, $post_id) ) {
643                 $return->error = esc_js( __('Unable to save the image.') );
644                 return $return;
645         }
646
647         if ( 'nothumb' == $target || 'all' == $target || 'full' == $target || $scaled ) {
648                 $tag = false;
649                 if ( isset($backup_sizes['full-orig']) ) {
650                         if ( ( !defined('IMAGE_EDIT_OVERWRITE') || !IMAGE_EDIT_OVERWRITE ) && $backup_sizes['full-orig']['file'] != $path_parts['basename'] )
651                                 $tag = "full-$suffix";
652                 } else {
653                         $tag = 'full-orig';
654                 }
655
656                 if ( $tag )
657                         $backup_sizes[$tag] = array('width' => $meta['width'], 'height' => $meta['height'], 'file' => $path_parts['basename']);
658
659                 $success = update_attached_file( $post_id, $new_path );
660
661                 $meta['file'] = _wp_relative_upload_path( $new_path );
662
663                 $size = $img->get_size();
664                 $meta['width'] = $size['width'];
665                 $meta['height'] = $size['height'];
666
667                 if ( $success && ('nothumb' == $target || 'all' == $target) ) {
668                         $sizes = get_intermediate_image_sizes();
669                         if ( 'nothumb' == $target )
670                                 $sizes = array_diff( $sizes, array('thumbnail') );
671                 }
672
673                 $return->fw = $meta['width'];
674                 $return->fh = $meta['height'];
675         } elseif ( 'thumbnail' == $target ) {
676                 $sizes = array( 'thumbnail' );
677                 $success = $delete = $nocrop = true;
678         }
679
680         if ( isset( $sizes ) ) {
681                 $_sizes = array();
682
683                 foreach ( $sizes as $size ) {
684                         $tag = false;
685                         if ( isset( $meta['sizes'][$size] ) ) {
686                                 if ( isset($backup_sizes["$size-orig"]) ) {
687                                         if ( ( !defined('IMAGE_EDIT_OVERWRITE') || !IMAGE_EDIT_OVERWRITE ) && $backup_sizes["$size-orig"]['file'] != $meta['sizes'][$size]['file'] )
688                                                 $tag = "$size-$suffix";
689                                 } else {
690                                         $tag = "$size-orig";
691                                 }
692
693                                 if ( $tag )
694                                         $backup_sizes[$tag] = $meta['sizes'][$size];
695                         }
696
697                         $crop = $nocrop ? false : get_option("{$size}_crop");
698                         $_sizes[ $size ] = array( 'width' => get_option("{$size}_size_w"), 'height' => get_option("{$size}_size_h"), 'crop' => $crop );
699                 }
700
701                 $meta['sizes'] = array_merge( $meta['sizes'], $img->multi_resize( $_sizes ) );
702         }
703
704         unset( $img );
705
706         if ( $success ) {
707                 wp_update_attachment_metadata( $post_id, $meta );
708                 update_post_meta( $post_id, '_wp_attachment_backup_sizes', $backup_sizes);
709
710                 if ( $target == 'thumbnail' || $target == 'all' || $target == 'full' ) {
711                         // Check if it's an image edit from attachment edit screen
712                         if ( ! empty( $_REQUEST['context'] ) && 'edit-attachment' == $_REQUEST['context'] ) {
713                                 $thumb_url = wp_get_attachment_image_src( $post_id, array( 900, 600 ), true );
714                                 $return->thumbnail = $thumb_url[0];
715                         } else {
716                                 $file_url = wp_get_attachment_url($post_id);
717                                 if ( $thumb = $meta['sizes']['thumbnail'] )
718                                         $return->thumbnail = path_join( dirname($file_url), $thumb['file'] );
719                                 else
720                                         $return->thumbnail = "$file_url?w=128&h=128";
721                         }
722                 }
723         } else {
724                 $delete = true;
725         }
726
727         if ( $delete ) {
728                 /** This filter is documented in wp-admin/custom-header.php */
729                 $delpath = apply_filters('wp_delete_file', $new_path);
730                 @unlink( $delpath );
731         }
732
733         $return->msg = esc_js( __('Image saved') );
734         return $return;
735 }