PluginProbe ʕ •ᴥ•ʔ
Modern Image Formats / 1.0.1
Modern Image Formats v1.0.1
2.7.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 2.0.0 2.0.1 2.0.2 2.1.0 2.2.0 2.3.0 2.4.0 2.5.0 2.5.1 2.6.0 2.6.1
webp-uploads / image-edit.php
webp-uploads Last commit date
can-load.php 2 years ago fallback.js 3 years ago helper.php 3 years ago hooks.php 2 years ago image-edit.php 2 years ago load.php 2 years ago readme.txt 2 years ago rest-api.php 3 years ago settings.php 3 years ago
image-edit.php
419 lines
1 <?php
2 /**
3 * Edit images integration for the module, including backup and restore support.
4 *
5 * @package webp-uploads
6 * @since 1.0.0
7 */
8
9 /**
10 * Adds sources to metadata for an attachment.
11 *
12 * @since 1.0.0
13 *
14 * @param array $metadata Metadata of the attachment.
15 * @param array $valid_mime_transforms List of valid mime transforms for current image mime type.
16 * @param array $main_images Path of all main image files of all mime types.
17 * @param array $subsized_images Path of all subsized image file of all mime types.
18 * @return array Metadata with sources added.
19 */
20 function webp_uploads_update_sources( $metadata, $valid_mime_transforms, $main_images, $subsized_images ) {
21 foreach ( $valid_mime_transforms as $targeted_mime ) {
22 // Make sure the path and file exists as those values are required.
23 $image_directory = null;
24 if ( isset( $main_images[ $targeted_mime ]['path'], $main_images[ $targeted_mime ]['file'] ) && file_exists( $main_images[ $targeted_mime ]['path'] ) ) {
25 // Add sources to original image metadata.
26 $metadata['sources'][ $targeted_mime ] = array(
27 'file' => $main_images[ $targeted_mime ]['file'],
28 'filesize' => wp_filesize( $main_images[ $targeted_mime ]['path'] ),
29 );
30 $image_directory = pathinfo( $main_images[ $targeted_mime ]['path'], PATHINFO_DIRNAME );
31 }
32
33 /**
34 * If no original image was provided the image_directory can't be determined, in that scenario try to
35 * find it from the `file` property.
36 *
37 * @see get_attached_file()
38 */
39 if (
40 null === $image_directory
41 && isset( $metadata['file'] )
42 && 0 !== strpos( $metadata['file'], '/' )
43 && ':\\' !== substr( $metadata['file'], 1, 2 )
44 ) {
45 $uploads = wp_get_upload_dir();
46 if ( false === $uploads['error'] && isset( $uploads['basedir'] ) ) {
47 $file = path_join( $uploads['basedir'], $metadata['file'] );
48 if ( file_exists( $file ) ) {
49 $image_directory = pathinfo( $file, PATHINFO_DIRNAME );
50 }
51 }
52 }
53
54 if ( null === $image_directory ) {
55 continue;
56 }
57
58 foreach ( $metadata['sizes'] as $size_name => $size_details ) {
59 if ( empty( $subsized_images[ $targeted_mime ][ $size_name ]['file'] ) ) {
60 continue;
61 }
62
63 // Add sources to resized image metadata.
64 $subsize_path = path_join( $image_directory, $subsized_images[ $targeted_mime ][ $size_name ]['file'] );
65 if ( ! file_exists( $subsize_path ) ) {
66 continue;
67 }
68
69 $metadata['sizes'][ $size_name ]['sources'][ $targeted_mime ] = array(
70 'file' => $subsized_images[ $targeted_mime ][ $size_name ]['file'],
71 'filesize' => wp_filesize( $subsize_path ),
72 );
73 }
74 }
75
76 return $metadata;
77 }
78
79 /**
80 * Creates additional image formats when original image is edited.
81 *
82 * @since 1.0.0
83 *
84 * @param bool|null $override Value to return instead of saving. Default null.
85 * @param string $file_path Name of the file to be saved.
86 * @param WP_Image_Editor $editor The image editor instance.
87 * @param string $mime_type The mime type of the image.
88 * @param int $post_id Attachment post ID.
89 * @return bool|null Potentially modified $override value.
90 */
91 function webp_uploads_update_image_onchange( $override, $file_path, $editor, $mime_type, $post_id ) {
92 if ( null !== $override ) {
93 return $override;
94 }
95
96 $transforms = webp_uploads_get_upload_image_mime_transforms();
97 if ( empty( $transforms[ $mime_type ] ) ) {
98 return $override;
99 }
100
101 $mime_transforms = $transforms[ $mime_type ];
102 // This variable allows to unhook the logic from within the closure without the need for a function name.
103 $callback_executed = false;
104 add_filter(
105 'wp_update_attachment_metadata',
106 static function ( $metadata, $post_meta_id ) use ( $post_id, $file_path, $mime_type, $editor, $mime_transforms, &$callback_executed ) {
107 if ( $post_meta_id !== $post_id ) {
108 return $metadata;
109 }
110
111 // This callback was already executed for this post, nothing to do at this point.
112 if ( $callback_executed ) {
113 return $metadata;
114 }
115 $callback_executed = true;
116 // No sizes to be created.
117 if ( empty( $metadata['sizes'] ) ) {
118 return $metadata;
119 }
120
121 $old_metadata = wp_get_attachment_metadata( $post_id );
122 $resize_sizes = array();
123 // PHPCS ignore reason: A nonce check is not necessary here as this logic directly ties in with WordPress core
124 // function `wp_ajax_image_editor()` which already has one.
125 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
126 $target = isset( $_REQUEST['target'] ) ? sanitize_key( $_REQUEST['target'] ) : 'all';
127
128 foreach ( $old_metadata['sizes'] as $size_name => $size_details ) {
129 // If the target is 'nothumb', skip generating the 'thumbnail' size.
130 if ( 'nothumb' === $target && 'thumbnail' === $size_name ) {
131 continue;
132 }
133
134 if ( isset( $metadata['sizes'][ $size_name ] ) && ! empty( $metadata['sizes'][ $size_name ] ) &&
135 $metadata['sizes'][ $size_name ]['file'] !== $old_metadata['sizes'][ $size_name ]['file'] ) {
136 $resize_sizes[ $size_name ] = $metadata['sizes'][ $size_name ];
137 }
138 }
139
140 $allowed_mimes = array_flip( wp_get_mime_types() );
141 $original_directory = pathinfo( $file_path, PATHINFO_DIRNAME );
142 $filename = pathinfo( $file_path, PATHINFO_FILENAME );
143 $main_images = array();
144 $subsized_images = array();
145
146 foreach ( $mime_transforms as $targeted_mime ) {
147 if ( $targeted_mime === $mime_type ) {
148 // If the target is `thumbnail` make sure it is the only selected size.
149 if ( 'thumbnail' === $target ) {
150 if ( isset( $metadata['sizes']['thumbnail'] ) ) {
151 $subsized_images[ $targeted_mime ] = array( 'thumbnail' => $metadata['sizes']['thumbnail'] );
152 }
153 // When the targeted thumbnail is selected no additional size and subsize is set.
154 continue;
155 }
156
157 $main_images[ $targeted_mime ] = array(
158 'path' => $file_path,
159 'file' => pathinfo( $file_path, PATHINFO_BASENAME ),
160 );
161 $subsized_images[ $targeted_mime ] = $metadata['sizes'];
162 continue;
163 }
164
165 if ( ! isset( $allowed_mimes[ $targeted_mime ] ) || ! is_string( $allowed_mimes[ $targeted_mime ] ) ) {
166 continue;
167 }
168
169 if ( ! $editor::supports_mime_type( $targeted_mime ) ) {
170 continue;
171 }
172
173 $extension = explode( '|', $allowed_mimes[ $targeted_mime ] );
174 $extension = $extension[0];
175
176 // If the target is `thumbnail` make sure only that size is generated.
177 if ( 'thumbnail' === $target ) {
178 if ( ! isset( $subsized_images[ $mime_type ]['thumbnail']['file'] ) ) {
179 continue;
180 }
181 $thumbnail_file = $subsized_images[ $mime_type ]['thumbnail']['file'];
182 $image_path = path_join( $original_directory, $thumbnail_file );
183 $editor = wp_get_image_editor( $image_path, array( 'mime_type' => $targeted_mime ) );
184
185 if ( is_wp_error( $editor ) ) {
186 continue;
187 }
188
189 $current_extension = pathinfo( $thumbnail_file, PATHINFO_EXTENSION );
190 // Create a file with then new extension out of the targeted file.
191 $target_file_name = preg_replace( "/\.$current_extension$/", ".$extension", $thumbnail_file );
192 $target_file_location = path_join( $original_directory, $target_file_name );
193
194 remove_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
195 $result = $editor->save( $target_file_location, $targeted_mime );
196 add_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
197
198 if ( is_wp_error( $result ) ) {
199 continue;
200 }
201
202 $subsized_images[ $targeted_mime ] = array( 'thumbnail' => $result );
203 } else {
204 $destination = trailingslashit( $original_directory ) . "{$filename}.{$extension}";
205
206 remove_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
207 $result = $editor->save( $destination, $targeted_mime );
208 add_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
209
210 if ( is_wp_error( $result ) ) {
211 continue;
212 }
213
214 $main_images[ $targeted_mime ] = $result;
215 $subsized_images[ $targeted_mime ] = $editor->multi_resize( $resize_sizes );
216 }
217 }
218
219 return webp_uploads_update_sources( $metadata, $mime_transforms, $main_images, $subsized_images );
220 },
221 10,
222 2
223 );
224
225 return $override;
226 }
227 add_filter( 'wp_save_image_editor_file', 'webp_uploads_update_image_onchange', 10, 5 );
228
229 /**
230 * Inspect if the current call to `wp_update_attachment_metadata()` was done from within the context
231 * of an edit to an attachment either restore or other type of edit, in that case we perform operations
232 * to save the sources properties, specifically for the `full` size image due this is a virtual image size.
233 *
234 * @since 1.0.0
235 *
236 * @see wp_update_attachment_metadata()
237 *
238 * @param array $data The current metadata of the attachment.
239 * @param int $attachment_id The ID of the current attachment.
240 * @return array The updated metadata for the attachment to be stored in the meta table.
241 */
242 function webp_uploads_update_attachment_metadata( $data, $attachment_id ) {
243 // PHPCS ignore reason: Update the attachment's metadata by either restoring or editing it.
244 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
245 $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 10 );
246
247 foreach ( $trace as $element ) {
248 if ( ! isset( $element['function'] ) ) {
249 continue;
250 }
251
252 switch ( $element['function'] ) {
253 case 'wp_save_image':
254 // Right after an image has been edited.
255 return webp_uploads_backup_sources( $attachment_id, $data );
256 case 'wp_restore_image':
257 // When an image has been restored.
258 $data = webp_uploads_backup_sources( $attachment_id, $data );
259 return webp_uploads_restore_image( $attachment_id, $data );
260 }
261 }
262
263 return $data;
264 }
265 add_filter( 'wp_update_attachment_metadata', 'webp_uploads_update_attachment_metadata', 10, 2 );
266
267 /**
268 * Before saving the metadata of the image store a backup values for the sources and file property
269 * those files would be used and deleted by the backup mechanism, right after the metadata has
270 * been updated. It removes the current sources property due once this function is executed
271 * right after an edit has taken place and the current sources are no longer accurate.
272 *
273 * @since 1.0.0
274 *
275 * @param int $attachment_id The ID representing the attachment.
276 * @param array $data The current metadata of the attachment.
277 * @return array The updated metadata for the attachment.
278 */
279 function webp_uploads_backup_sources( $attachment_id, $data ) {
280 // PHPCS ignore reason: A nonce check is not necessary here as this logic directly ties in with WordPress core
281 // function `wp_ajax_image_editor()` which already has one.
282 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
283 $target = isset( $_REQUEST['target'] ) ? sanitize_key( $_REQUEST['target'] ) : 'all';
284
285 // When an edit to an image is only applied to a thumbnail there's nothing we need to back up.
286 if ( 'thumbnail' === $target ) {
287 return $data;
288 }
289
290 $metadata = wp_get_attachment_metadata( $attachment_id );
291 // Nothing to back up.
292 if ( ! isset( $metadata['sources'] ) ) {
293 return $data;
294 }
295
296 $sources = $metadata['sources'];
297 // Prevent execution of the callbacks more than once if the callback was already executed.
298 $has_been_processed = false;
299
300 $hook = static function ( $meta_id, $post_id, $meta_name ) use ( $attachment_id, $sources, &$has_been_processed ) {
301 // Make sure this hook is only executed in the same context for the provided $attachment_id.
302 if ( $post_id !== $attachment_id ) {
303 return;
304 }
305
306 // This logic should work only if we are looking at the meta key: `_wp_attachment_backup_sizes`.
307 if ( '_wp_attachment_backup_sizes' !== $meta_name ) {
308 return;
309 }
310
311 if ( $has_been_processed ) {
312 return;
313 }
314
315 $has_been_processed = true;
316 webp_uploads_backup_full_image_sources( $post_id, $sources );
317 };
318
319 add_action( 'added_post_meta', $hook, 10, 3 );
320 add_action( 'updated_post_meta', $hook, 10, 3 );
321
322 // Remove the current sources as at this point the current values are no longer accurate.
323 // TODO: Requires to be updated from https://github.com/WordPress/performance/issues/158.
324 unset( $data['sources'] );
325
326 return $data;
327 }
328
329 /**
330 * Stores the provided sources for the attachment ID in the `_wp_attachment_backup_sources` with
331 * the next available target if target is `null` no source would be stored.
332 *
333 * @since 1.0.0
334 *
335 * @param int $attachment_id The ID of the attachment.
336 * @param array $sources An array with the full sources to be stored on the next available key.
337 */
338 function webp_uploads_backup_full_image_sources( $attachment_id, $sources ) {
339 if ( empty( $sources ) ) {
340 return;
341 }
342
343 $target = webp_uploads_get_next_full_size_key_from_backup( $attachment_id );
344 if ( null === $target ) {
345 return;
346 }
347
348 $backup_sources = get_post_meta( $attachment_id, '_wp_attachment_backup_sources', true );
349 $backup_sources = is_array( $backup_sources ) ? $backup_sources : array();
350 $backup_sources[ $target ] = $sources;
351 // Store the `sources` property into the full size if present.
352 update_post_meta( $attachment_id, '_wp_attachment_backup_sources', $backup_sources );
353 }
354
355 /**
356 * It finds the next available `full-{orig or hash}` key on the images if the name
357 * has not been used as part of the backup sources it would be used if no size is
358 * found or backup exists `null` would be returned instead.
359 *
360 * @since 1.0.0
361 *
362 * @param int $attachment_id The ID of the attachment.
363 * @return null|string The next available full size name.
364 */
365 function webp_uploads_get_next_full_size_key_from_backup( $attachment_id ) {
366 $backup_sizes = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true );
367 $backup_sizes = is_array( $backup_sizes ) ? $backup_sizes : array();
368
369 if ( empty( $backup_sizes ) ) {
370 return null;
371 }
372
373 $backup_sources = get_post_meta( $attachment_id, '_wp_attachment_backup_sources', true );
374 $backup_sources = is_array( $backup_sources ) ? $backup_sources : array();
375 foreach ( array_keys( $backup_sizes ) as $size_name ) {
376 // If the target already has the sources attributes find the next one.
377 if ( isset( $backup_sources[ $size_name ] ) ) {
378 continue;
379 }
380
381 // We are only interested in the `full-` sizes.
382 if ( strpos( $size_name, 'full-' ) === false ) {
383 continue;
384 }
385
386 return $size_name;
387 }
388
389 return null;
390 }
391
392 /**
393 * Restore an image from the backup sizes, the current hook moves the `sources` from the `full-orig` key into
394 * the top level `sources` into the metadata, in order to ensure the restore process has a reference to the right
395 * images.
396 *
397 * @since 1.0.0
398 *
399 * @param int $attachment_id The ID of the attachment.
400 * @param array $data The current metadata to be stored in the attachment.
401 * @return array The updated metadata of the attachment.
402 */
403 function webp_uploads_restore_image( $attachment_id, $data ) {
404 $backup_sources = get_post_meta( $attachment_id, '_wp_attachment_backup_sources', true );
405 if ( ! is_array( $backup_sources ) ) {
406 $backup_sources = array();
407 }
408
409 if ( ! isset( $backup_sources['full-orig'] ) || ! is_array( $backup_sources['full-orig'] ) ) {
410 return $data;
411 }
412
413 // TODO: Handle the case If `IMAGE_EDIT_OVERWRITE` is defined and is truthy remove any edited images if present before replacing the metadata.
414 // See: https://github.com/WordPress/performance/issues/158.
415 $data['sources'] = $backup_sources['full-orig'];
416
417 return $data;
418 }
419