PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / trunk
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF vtrunk
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
← All changes | classes/Optimization/Process/AbstractProcess.php +738 -557 1.10 → trunk View file →
@@ -1,37 +1,30 @@
1 1 <?php
2 +declare(strict_types=1);
3 +
2 4 namespace Imagify\Optimization\Process;
3 5
6 +use Imagify_Filesystem;
7 +use Imagify\Deprecated\Traits\Optimization\Process\AbstractProcessDeprecatedTrait;
4 8 use Imagify\Job\MediaOptimization;
5 9 use Imagify\Optimization\Data\DataInterface;
6 10 use Imagify\Optimization\File;
11 +use Imagify\Media\MediaInterface;
12 +use WP_Error;
7 13
8 -defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' );
9 -
10 14 /**
11 15 * Abstract class used to optimize medias.
12 16 *
13 - * @since 1.9
14 - * @author Grégory Viguier
17 + * @since 1.9
15 18 */
16 19 abstract class AbstractProcess implements ProcessInterface {
17 - use \Imagify\Deprecated\Traits\Optimization\Process\AbstractProcessDeprecatedTrait;
20 + use AbstractProcessDeprecatedTrait;
18 21
19 22 /**
20 - * The suffix used in the thumbnail size name.
21 - *
22 - * @var string
23 - * @since 1.9
24 - * @author Grégory Viguier
25 - */
26 - const WEBP_SUFFIX = '@imagify-webp';
27 -
28 - /**
29 23 * The suffix used in file name to create a temporary copy of the full size.
30 24 *
31 - * @var string
32 - * @since 1.9
33 - * @author Grégory Viguier
25 + * @var string
26 + * @since 1.9
34 27 */
35 28 const TMP_SUFFIX = '@imagify-tmp';
36 29
37 30 /**
@@ -37,11 +30,10 @@
37 30 /**
38 31 * Used for the name of the transient telling if a media is locked.
39 32 * %1$s is the context, %2$s is the media ID.
40 33 *
41 - * @var string
42 - * @since 1.9
43 - * @author Grégory Viguier
34 + * @var string
35 + * @since 1.9
44 36 */
45 37 const LOCK_NAME = 'imagify_%1$s_%2$s_process_locked';
46 38
47 39 /**
@@ -46,12 +38,10 @@
46 38
47 39 /**
48 40 * The data optimization object.
49 41 *
50 - * @var DataInterface
51 - * @since 1.9
52 - * @access protected
53 - * @author Grégory Viguier
42 + * @var DataInterface
43 + * @since 1.9
54 44 */
55 45 protected $data;
56 46
57 47 /**
@@ -56,12 +46,10 @@
56 46
57 47 /**
58 48 * The optimization data format.
59 49 *
60 - * @var array
61 - * @since 1.9
62 - * @access protected
63 - * @author Grégory Viguier
50 + * @var array
51 + * @since 1.9
64 52 */
65 53 protected $data_format = [
66 54 'level' => null,
67 55 'status' => null,
@@ -73,12 +61,10 @@
73 61
74 62 /**
75 63 * A File instance.
76 64 *
77 - * @var File
78 - * @since 1.9
79 - * @access protected
80 - * @author Grégory Viguier
65 + * @var File
66 + * @since 1.9
81 67 */
82 68 protected $file;
83 69
84 70 /**
@@ -83,12 +69,10 @@
83 69
84 70 /**
85 71 * Filesystem object.
86 72 *
87 - * @var Imagify_Filesystem
88 - * @since 1.9
89 - * @access protected
90 - * @author Grégory Viguier
73 + * @var Imagify_Filesystem
74 + * @since 1.9
91 75 */
92 76 protected $filesystem;
93 77
94 78 /**
@@ -93,22 +77,36 @@
93 77
94 78 /**
95 79 * Used to cache the plugin’s options.
96 80 *
97 - * @var array
98 - * @since 1.9
99 - * @access protected
100 - * @author Grégory Viguier
81 + * @var array
82 + * @since 1.9
101 83 */
102 84 protected $options = [];
103 85
104 86 /**
87 + * Tells the format we are currently processing
88 + *
89 + * @var string
90 + * @since 2.2
91 + */
92 + protected $format;
93 +
94 + /**
95 + * Array of image extensions processed.
96 + *
97 + * @var array
98 + */
99 + protected $extensions = [
100 + 'webp',
101 + 'avif',
102 + ];
103 +
104 + /**
105 105 * The constructor.
106 106 *
107 - * @since 1.9
108 - * @access public
109 - * @see self::constructor_accepts()
110 - * @author Grégory Viguier
107 + * @since 1.9
108 + * @see self::constructor_accepts()
111 109 *
112 110 * @param mixed $id An ID, or whatever type the constructor accepts.
113 111 */
114 112 public function __construct( $id ) {
@@ -122,18 +120,18 @@
122 120 $this->data = false;
123 121 }
124 122
125 123 $this->filesystem = \Imagify_Filesystem::get_instance();
124 + $this->format = $this->get_current_format();
126 125 }
127 126
128 127 /**
129 128 * Tell if the given entry can be accepted in the constructor.
130 129 *
131 - * @since 1.9
132 - * @access public
133 - * @author Grégory Viguier
130 + * @since 1.9
134 131 *
135 - * @param mixed $id Whatever.
132 + * @param mixed $id Whatever.
133 + *
136 134 * @return bool
137 135 */
138 136 public static function constructor_accepts( $id ) {
139 137 if ( $id instanceof DataInterface ) {
@@ -148,11 +146,9 @@
148 146
149 147 /**
150 148 * Get the data instance.
151 149 *
152 - * @since 1.9
153 - * @access public
154 - * @author Grégory Viguier
150 + * @since 1.9
155 151 *
156 152 * @return DataInterface|false
157 153 */
158 154 public function get_data() {
@@ -161,11 +157,9 @@
161 157
162 158 /**
163 159 * Get the media instance.
164 160 *
165 - * @since 1.9
166 - * @access public
167 - * @author Grégory Viguier
161 + * @since 1.9
168 162 *
169 163 * @return MediaInterface|false
170 164 */
171 165 public function get_media() {
@@ -178,11 +172,9 @@
178 172
179 173 /**
180 174 * Get the File instance of the original file.
181 175 *
182 - * @since 1.9.8
183 - * @access public
184 - * @author Grégory Viguier
176 + * @since 1.9.8
185 177 *
186 178 * @return File|false
187 179 */
188 180 public function get_original_file() {
@@ -201,11 +193,9 @@
201 193
202 194 /**
203 195 * Get the File instance of the full size file.
204 196 *
205 - * @since 1.9.8
206 - * @access public
207 - * @author Grégory Viguier
197 + * @since 1.9.8
208 198 *
209 199 * @return File|false
210 200 */
211 201 public function get_fullsize_file() {
@@ -224,11 +214,9 @@
224 214
225 215 /**
226 216 * Tell if the current media is valid.
227 217 *
228 - * @since 1.9
229 - * @access public
230 - * @author Grégory Viguier
218 + * @since 1.9
231 219 *
232 220 * @return bool
233 221 */
234 222 public function is_valid() {
@@ -237,13 +225,11 @@
237 225
238 226 /**
239 227 * Tell if the current user is allowed to operate Imagify in this context.
240 228 *
241 - * @since 1.9
242 - * @access public
243 - * @author Grégory Viguier
229 + * @since 1.9
244 230 *
245 - * @param string $describer Capacity describer. See \Imagify\Context\ContextInterface->get_capacity() for possible values. Can also be a "real" user capacity.
231 + * @param string $describer Capacity describer. See \Imagify\Context\ContextInterface->get_capacity() for possible values. Can also be a "real" user capacity.
246 232 * @return bool
247 233 */
248 234 public function current_user_can( $describer ) {
249 235 if ( ! $this->is_valid() ) {
@@ -254,48 +240,46 @@
254 240
255 241 return $media->get_context_instance()->current_user_can( $describer, $media->get_id() );
256 242 }
257 243
258 -
259 - /** ----------------------------------------------------------------------------------------- */
260 - /** OPTIMIZATION ============================================================================ */
261 - /** ----------------------------------------------------------------------------------------- */
262 -
263 244 /**
264 245 * Optimize a media files.
265 246 *
266 - * @since 1.9
267 - * @access public
268 - * @author Grégory Viguier
247 + * @since 1.9
248 + * @since 2.3.2 Added the $args parameter (e.g. 'bulk', 'priority'). See self::optimize_sizes()
249 + * for the full list. 'bulk'/'priority' are extracted before the item is queued,
250 + * any other key ends up in the queued item's 'data', forwarded verbatim to the
251 + * 'imagify_before_*'/'imagify_after_*' hooks.
269 252 *
270 - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
271 - * @param array $args An array of optionnal arguments.
272 - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
253 + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
254 + * @param array $args An array of optionnal arguments.
255 + *
256 + * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
273 257 */
274 258 public function optimize( $optimization_level = null, $args = [] ) {
275 259 if ( ! $this->is_valid() ) {
276 - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
260 + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
277 261 }
278 262
279 263 $media = $this->get_media();
280 264
281 265 if ( ! $media->is_supported() ) {
282 - return new \WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
266 + return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
283 267 }
284 268
285 269 $data = $this->get_data();
286 270
287 271 if ( $data->is_optimized() ) {
288 - return new \WP_Error( 'optimized', __( 'This media has already been optimized by Imagify.', 'imagify' ) );
272 + return new WP_Error( 'optimized', __( 'This media has already been optimized by Imagify.', 'imagify' ) );
289 273 }
290 274
291 - if ( $data->is_already_optimized() && $this->has_webp() ) {
292 - // If already optimized but has WebP, delete WebP versions and optimization data.
275 + if ( $data->is_already_optimized() && $this->has_next_gen() ) {
276 + // If already optimized but has next-gen, delete next-gen versions and optimization data.
293 277 $data->delete_optimization_data();
294 - $deleted = $this->delete_webp_files();
278 + $deleted = $this->delete_nextgen_files();
295 279
296 280 if ( is_wp_error( $deleted ) ) {
297 - return new \WP_Error( 'webp_not_deleted', __( 'Previous WebP files could not be deleted.', 'imagify' ) );
281 + return new WP_Error( 'next_gen_not_deleted', __( 'Previous Next-Gen files could not be deleted.', 'imagify' ) );
298 282 }
299 283 }
300 284
301 285 $sizes = $media->get_media_files();
@@ -309,37 +293,40 @@
309 293
310 294 /**
311 295 * Re-optimize a media files with a different level.
312 296 *
313 - * @since 1.9
314 - * @access public
315 - * @author Grégory Viguier
297 + * @since 1.9
298 + * @since 2.3.2 Added the $args parameter (e.g. 'bulk', 'priority'). See self::optimize_sizes()
299 + * for the full list. 'bulk'/'priority' are extracted before the item is queued,
300 + * any other key ends up in the queued item's 'data', forwarded verbatim to the
301 + * 'imagify_before_*'/'imagify_after_*' hooks.
316 302 *
317 - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
318 - * @param array $args An array of optionnal arguments.
319 - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
303 + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
304 + * @param array $args An array of optionnal arguments.
305 + *
306 + * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
320 307 */
321 308 public function reoptimize( $optimization_level = null, $args = [] ) {
322 309 if ( ! $this->is_valid() ) {
323 - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
310 + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
324 311 }
325 312
326 313 $media = $this->get_media();
327 314
328 315 if ( ! $media->is_supported() ) {
329 - return new \WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
316 + return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
330 317 }
331 318
332 319 $data = $this->get_data();
333 320
334 321 if ( ! $data->get_optimization_status() ) {
335 - return new \WP_Error( 'not_processed_yet', __( 'This media has not been processed yet.', 'imagify' ) );
322 + return new WP_Error( 'not_processed_yet', __( 'This media has not been processed yet.', 'imagify' ) );
336 323 }
337 324
338 325 $optimization_level = $this->sanitize_optimization_level( $optimization_level );
339 326
340 327 if ( $data->get_optimization_level() === $optimization_level ) {
341 - return new \WP_Error( 'identical_optimization_level', __( 'This media is already optimized with this level.', 'imagify' ) );
328 + return new WP_Error( 'identical_optimization_level', __( 'This media is already optimized with this level.', 'imagify' ) );
342 329 }
343 330
344 331 $this->restore();
345 332
@@ -354,41 +341,45 @@
354 341
355 342 /**
356 343 * Optimize several file sizes by pushing tasks into the queue.
357 344 *
358 - * @since 1.9
359 - * @access public
360 - * @see MediaOptimization->task_before()
361 - * @see MediaOptimization->task_after()
362 - * @author Grégory Viguier
345 + * @since 1.9
346 + * @see MediaOptimization->task_before()
347 + * @see MediaOptimization->task_after()
363 348 *
364 - * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file.
365 - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
366 - * @param array $args {
367 - * An array of optionnal arguments.
349 + * @since 2.2
350 + * Addition of the image format
368 351 *
352 + * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file.
353 + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
354 + * @param array $args {
355 + * An array of optionnal arguments.
356 + *
369 357 * @type string $hook_suffix Suffix used to trigger hooks before and after optimization.
358 + * @type bool $priority Whether this optimization should jump ahead of the queue (e.g. new uploads, manual clicks). Default false. Extracted before queueing: not part of the queued item's 'data'.
359 + * @type bool $bulk Whether this optimization was triggered by a bulk run (see Bulk::force_optimize()). Default false. Kept in the queued item's 'data', so it is forwarded verbatim to the 'imagify_before_*'/'imagify_after_*' hook callbacks.
370 360 * }
371 - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
361 + *
362 + * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
372 363 */
373 364 public function optimize_sizes( $sizes, $optimization_level = null, $args = [] ) {
374 365 if ( ! $this->is_valid() ) {
375 - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
366 + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
376 367 }
377 368
378 369 $media = $this->get_media();
379 370
380 371 if ( ! $media->is_supported() ) {
381 - return new \WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
372 + return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
382 373 }
383 374
384 375 if ( ! $sizes ) {
385 - return new \WP_Error( 'no_sizes', __( 'No sizes given to be optimized.', 'imagify' ) );
376 + return new WP_Error( 'no_sizes', __( 'No sizes given to be optimized.', 'imagify' ) );
386 377 }
387 378
388 379 if ( empty( $args['locked'] ) ) {
389 380 if ( $this->is_locked() ) {
390 - return new \WP_Error( 'media_locked', __( 'This media is already being processed.', 'imagify' ) );
381 + return new WP_Error( 'media_locked', __( 'This media is already being processed.', 'imagify' ) );
391 382 }
392 383
393 384 $this->lock();
394 385 }
@@ -393,10 +384,18 @@
393 384 $this->lock();
394 385 }
395 386
396 387 if ( $media->is_image() ) {
397 - if ( $this->get_option( 'convert_to_webp' ) ) {
398 - // Add WebP convertion.
388 + // Add Next-Gen conversion.
389 + $formats = imagify_nextgen_images_formats();
390 +
391 + foreach ( $formats as $format ) {
392 + if ( 'avif' === $format ) {
393 + $format_suffix = static::AVIF_SUFFIX;
394 + } elseif ( 'webp' === $format ) {
395 + $format_suffix = static::WEBP_SUFFIX;
396 + }
397 +
399 398 $files = $media->get_media_files();
400 399
401 400 foreach ( $sizes as $size_name ) {
402 401 if ( empty( $files[ $size_name ] ) ) {
@@ -401,16 +400,18 @@
401 400 foreach ( $sizes as $size_name ) {
402 401 if ( empty( $files[ $size_name ] ) ) {
403 402 continue;
404 403 }
405 - if ( 'image/webp' === $files[ $size_name ]['mime-type'] ) {
404 +
405 + if ( $this->get_mime_type( $format ) === $files[ $size_name ]['mime-type'] ) {
406 406 continue;
407 407 }
408 - if ( in_array( $size_name . static::WEBP_SUFFIX, $sizes, true ) ) {
408 +
409 + if ( in_array( $size_name . $format_suffix, $sizes, true ) ) {
409 410 continue;
410 411 }
411 412
412 - array_unshift( $sizes, $size_name . static::WEBP_SUFFIX );
413 + array_unshift( $sizes, $size_name . $format_suffix );
413 414 }
414 415 }
415 416
416 417 if ( ! $media->get_context_instance()->can_backup() && ! $media->get_backup_path() && ! $this->get_data()->get_size_data( 'full', 'success' ) ) {
@@ -415,25 +416,24 @@
415 416
416 417 if ( ! $media->get_context_instance()->can_backup() && ! $media->get_backup_path() && ! $this->get_data()->get_size_data( 'full', 'success' ) ) {
417 418 /**
418 419 * Backup is NOT activated, and a backup file does NOT exist yet, and the full size is NOT optimized yet.
419 - * WebP conversion needs a backup file, even a temporary one: we’ll create one.
420 + * Next-Gen conversion needs a backup file, even a temporary one: we’ll create one.
420 421 */
421 - $webp = false;
422 + $next_gen = false;
422 423
423 424 foreach ( $sizes as $size_name ) {
424 - if ( $this->is_size_webp( $size_name ) ) {
425 - $webp = true;
425 + if ( $this->is_size_next_gen( $size_name ) ) {
426 + $next_gen = true;
426 427 break;
427 428 }
428 429 }
429 430
430 - if ( $webp ) {
431 - // We have at least one WebP conversion to do: create a temporary backup.
431 + if ( $next_gen ) {
432 + // We have at least one next-gen conversion to do: create a temporary backup.
432 433 $backuped = $this->get_original_file()->backup( $media->get_raw_backup_path() );
433 434
434 435 if ( $backuped ) {
435 - // See \Imagify\Job\MediaOptimization->delete_backup().
436 436 $args['delete_backup'] = true;
437 437 }
438 438 }
439 439 }
@@ -444,10 +444,9 @@
444 444
445 445 /**
446 446 * Filter the data sent to the optimization process.
447 447 *
448 - * @since 1.9
449 - * @author Grégory Viguier
448 + * @since 1.9
450 449 *
451 450 * @param array $new_args Additional data to send to the optimization process.
452 451 * @param array $args Current data sent to the process.
453 452 * @param ProcessInterface $process The current optimization process.
@@ -459,20 +458,32 @@
459 458 if ( $new_args && is_array( $new_args ) ) {
460 459 $args = array_merge( $new_args, $args );
461 460 }
462 461
462 + // 'priority' is a queue-mechanics flag: it must be a sibling key of 'id'/'sizes'/etc.,
463 + // not nested inside 'data' (which is opaque payload forwarded verbatim to the
464 + // 'imagify_before_*'/'imagify_after_*' hook callbacks). Extract it before building 'data'.
465 + $is_priority = ! empty( $args['priority'] );
466 + unset( $args['priority'] );
467 +
463 468 /**
464 469 * Push the item to the queue, save the queue in the DB, empty the queue.
465 470 * A "batch" is then created in the DB with this unique item, it is then free to loop through its steps (files) without another item interfering (each media optimization has its own dedicated batch/queue).
466 471 */
467 - MediaOptimization::get_instance()->push_to_queue( [
472 + $item = [
468 473 'id' => $media->get_id(),
469 474 'sizes' => $sizes,
470 475 'optimization_level' => $optimization_level,
471 476 'process_class' => get_class( $this ),
472 477 'data' => $args,
473 - ] )->save();
478 + ];
474 479
480 + if ( $is_priority ) {
481 + $item['priority'] = true;
482 + }
483 +
484 + MediaOptimization::get_instance()->push_to_queue( $item )->save();
485 +
475 486 return true;
476 487 }
477 488
478 489 /**
@@ -477,39 +488,38 @@
477 488
478 489 /**
479 490 * Optimize one file with Imagify directly.
480 491 *
481 - * @since 1.9
482 - * @access public
483 - * @author Grégory Viguier
492 + * @since 1.9
484 493 *
485 - * @param string $size The media size.
486 - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
487 - * @return array|\WP_Error Optimized image data. A \WP_Error object on error.
494 + * @param string $size The media size.
495 + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
496 + *
497 + * @return array|WP_Error Optimized image data. A WP_Error object on error.
488 498 */
489 499 public function optimize_size( $size, $optimization_level = null ) {
490 500 if ( ! $this->is_valid() ) { // Bail out.
491 - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
501 + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
492 502 }
493 503
494 504 $media = $this->get_media();
495 505 $sizes = $media->get_media_files();
496 506 $thumb_size = $size;
497 - $webp = $this->is_size_webp( $size );
507 + $next_gen = $this->is_size_next_gen( $size );
498 508 $path_is_temp = false;
499 509
500 - if ( $webp ) {
510 + if ( $next_gen ) {
501 511 // We'll make sure the file is an image later.
502 - $thumb_size = $webp; // Contains the name of the non-WebP size.
503 - $webp = true;
512 + $thumb_size = $next_gen; // Contains the name of the non-next-gen size.
513 + $next_gen = true;
504 514 }
505 515
506 516 if ( empty( $sizes[ $thumb_size ]['path'] ) ) { // Bail out.
507 517 // This size is not in our list.
508 - return new \WP_Error(
518 + return new WP_Error(
509 519 'unknown_size',
510 520 sprintf(
511 - /* translators: %s is a size name. */
521 + /* translators: %s is a size name. */
512 522 __( 'The size %s is unknown.', 'imagify' ),
513 523 '<code>' . esc_html( $thumb_size ) . '</code>'
514 524 )
515 525 );
@@ -514,28 +524,38 @@
514 524 )
515 525 );
516 526 }
517 527
518 - if ( $this->get_data()->get_size_data( $size, 'success' ) ) { // Bail out.
519 - // This size is already optimized with Imagify, and must not be optimized again.
520 - if ( $webp ) {
521 - return new \WP_Error(
522 - 'size_is_successfully_optimized',
523 - sprintf(
528 + if ( $this->get_data()->get_size_data( $size, 'success' ) ) {
529 + /*
530 + * The data says this size is already optimized. For a next-gen size, this can be
531 + * stale: an API response carrying a `message` (see File::optimize()) could have
532 + * been recorded as `success` without any next-gen file ever being written. In that
533 + * case, don't bail out: fall through and let the file be (re)generated.
534 + */
535 + $next_gen_file_is_missing = $next_gen && $this->is_next_gen_file_missing( $size, $sizes[ $thumb_size ]['path'] );
536 +
537 + if ( ! $next_gen_file_is_missing ) { // Bail out.
538 + // This size is already optimized with Imagify, and must not be optimized again.
539 + if ( $next_gen ) {
540 + return new WP_Error(
541 + 'size_is_successfully_optimized',
542 + sprintf(
524 543 /* translators: %s is a size name. */
525 - __( 'The WebP format for the size %s already exists.', 'imagify' ),
526 - '<code>' . esc_html( $thumb_size ) . '</code>'
527 - )
528 - );
529 - } else {
530 - return new \WP_Error(
531 - 'size_is_successfully_optimized',
532 - sprintf(
544 + __( 'The Next-Gen format for the size %s already exists.', 'imagify' ),
545 + '<code>' . esc_html( $thumb_size ) . '</code>'
546 + )
547 + );
548 + } else {
549 + return new WP_Error(
550 + 'size_is_successfully_optimized',
551 + sprintf(
533 552 /* translators: %s is a size name. */
534 - __( 'The size %s is already optimized by Imagify.', 'imagify' ),
535 - '<code>' . esc_html( $thumb_size ) . '</code>'
536 - )
537 - );
553 + __( 'The size %s is already optimized by Imagify.', 'imagify' ),
554 + '<code>' . esc_html( $thumb_size ) . '</code>'
555 + )
556 + );
557 + }
538 558 }
539 559 }
540 560
541 561 /**
@@ -544,18 +564,18 @@
544 564 $path = $sizes[ $thumb_size ]['path'];
545 565
546 566 $optimization_level = $this->sanitize_optimization_level( $optimization_level );
547 567
548 - if ( $webp && $this->get_data()->get_size_data( $thumb_size, 'success' ) ) {
549 - // We want a WebP version but the source file is already optimized by Imagify.
568 + if ( $next_gen && $this->get_data()->get_size_data( $thumb_size, 'success' ) ) {
569 + // We want a next-gen version but the source file is already optimized by Imagify.
550 570 $result = $this->create_temporary_copy( $thumb_size, $sizes );
551 571
552 572 if ( ! $result ) { // Bail out.
553 - // Could not create a copy of the non-WebP version.
554 - $response = new \WP_Error(
555 - 'non_webp_copy_failed',
573 + // Could not create a copy of the non-next-gen version.
574 + $response = new WP_Error(
575 + 'non_next_gen_copy_failed',
556 576 sprintf(
557 - /* translators: %s is a size name. */
577 + /* translators: %s is a size name. */
558 578 __( 'Could not create an unoptimized copy of the size %s.', 'imagify' ),
559 579 '<code>' . esc_html( $thumb_size ) . '</code>'
560 580 )
561 581 );
@@ -577,18 +597,28 @@
577 597 if ( ! $file->is_supported( $media->get_allowed_mime_types() ) ) { // Bail out.
578 598 // This file type is not supported.
579 599 $extension = $file->get_extension();
580 600
581 - if ( '' === $extension ) {
582 - $response = new \WP_Error(
601 + if ( ! $extension ) {
602 + $response = new WP_Error(
603 + 'extension_not_mime',
604 + __( 'This file has an extension that does not match a mime type.', 'imagify' )
605 + );
606 + } elseif ( '' === $extension ) {
607 + $response = new WP_Error(
583 608 'no_extension',
584 609 __( 'With no extension, this file cannot be optimized.', 'imagify' )
585 610 );
611 + } elseif ( ! $extension ) {
612 + $response = new WP_Error(
613 + 'extension_not_mime',
614 + __( 'This file has an extension that does not match a mime type.', 'imagify' )
615 + );
586 616 } else {
587 - $response = new \WP_Error(
617 + $response = new WP_Error(
588 618 'extension_not_supported',
589 619 sprintf(
590 - /* translators: %s is a file extension. */
620 + /* translators: %s is a file extension. */
591 621 __( '%s cannot be optimized.', 'imagify' ),
592 622 '<code>' . esc_html( strtolower( $extension ) ) . '</code>'
593 623 )
594 624 );
@@ -602,16 +632,16 @@
602 632
603 633 return $response;
604 634 }
605 635
606 - if ( $webp && ! $file->is_image() ) { // Bail out.
636 + if ( $next_gen && ! $file->is_image() ) { // Bail out.
607 637 if ( $path_is_temp ) {
608 638 $this->filesystem->delete( $path );
609 639 }
610 640
611 - $response = new \WP_Error(
612 - 'no_webp',
613 - __( 'This file is not an image and cannot be converted to WebP format.', 'imagify' )
641 + $response = new WP_Error(
642 + 'no_next_gen',
643 + __( 'This file is not an image and cannot be converted to Next-Gen format.', 'imagify' )
614 644 );
615 645
616 646 $this->update_size_optimization_data( $response, $size, $optimization_level );
617 647
@@ -621,14 +651,13 @@
621 651 $is_disabled = ! empty( $sizes[ $thumb_size ]['disabled'] );
622 652
623 653 /**
624 654 * Fires before optimizing a file.
625 - * Return a \WP_Error object to prevent the optimization.
655 + * Return a WP_Error object to prevent the optimization.
626 656 *
627 - * @since 1.9
628 - * @author Grégory Viguier
657 + * @since 1.9
629 658 *
630 - * @param null|\WP_Error $response Null by default. Return a \WP_Error object to prevent optimization.
659 + * @param null|WP_Error $response Null by default. Return a WP_Error object to prevent optimization.
631 660 * @param ProcessInterface $process The optimization process instance.
632 661 * @param File $file The file instance. If $webp is true, $file references the non-WebP file.
633 662 * @param string $thumb_size The media size.
634 663 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
@@ -634,40 +663,40 @@
634 663 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
635 664 * @param bool $webp The image will be converted to WebP.
636 665 * @param bool $is_disabled Tell if this size is disabled from optimization.
637 666 */
638 - $response = apply_filters( 'imagify_before_optimize_size', null, $this, $file, $thumb_size, $optimization_level, $webp, $is_disabled );
667 + $response = apply_filters( 'imagify_before_optimize_size', null, $this, $file, $thumb_size, $optimization_level, $next_gen, $is_disabled );
639 668
640 669 if ( ! is_wp_error( $response ) ) {
641 670 if ( $is_disabled ) {
642 671 // This size must not be optimized.
643 - $response = new \WP_Error(
672 + $response = new WP_Error(
644 673 'unauthorized_size',
645 674 sprintf(
646 - /* translators: %s is a size name. */
675 + /* translators: %s is a size name. */
647 676 __( 'The size %s is not authorized to be optimized. Update your Imagify settings if you want to optimize it.', 'imagify' ),
648 677 '<code>' . esc_html( $thumb_size ) . '</code>'
649 678 )
650 679 );
651 680 } elseif ( ! $this->filesystem->exists( $file->get_path() ) ) {
652 - $response = new \WP_Error(
681 + $response = new WP_Error(
653 682 'file_not_exists',
654 683 sprintf(
655 - /* translators: %s is a file path. */
684 + /* translators: %s is a file path. */
656 685 __( 'The file %s does not seem to exist.', 'imagify' ),
657 686 '<code>' . esc_html( $this->filesystem->make_path_relative( $file->get_path() ) ) . '</code>'
658 687 )
659 688 );
660 - } elseif ( $webp && ! $this->can_create_webp_version( $file->get_path() ) ) {
661 - $response = new \WP_Error(
689 + } elseif ( $next_gen && ! $this->can_create_next_gen_version( $file->get_path() ) ) {
690 + $response = new WP_Error(
662 691 'is_animated_gif',
663 - __( 'This file is an animated gif: since Imagify does not support animated WebP, WebP creation for animated gif is disabled.', 'imagify' )
692 + __( 'This file is an animated gif: since Imagify does not support animated WebP/AVIF, WebP/AVIF creation for animated gif is disabled.', 'imagify' )
664 693 );
665 694 } elseif ( ! $this->filesystem->is_writable( $file->get_path() ) ) {
666 - $response = new \WP_Error(
695 + $response = new WP_Error(
667 696 'file_not_writable',
668 697 sprintf(
669 - /* translators: %s is a file path. */
698 + /* translators: %s is a file path. */
670 699 __( 'The file %s does not seem to be writable.', 'imagify' ),
671 700 '<code>' . esc_html( $this->filesystem->make_path_relative( $file->get_path() ) ) . '</code>'
672 701 )
673 702 );
@@ -674,29 +703,52 @@
674 703 } else {
675 704 // Maybe resize the file.
676 705 $response = $this->maybe_resize( $thumb_size, $file );
677 706
707 + $convert = '';
708 +
709 + if ( $next_gen ) {
710 + if ( strpos( $size, static::AVIF_SUFFIX ) ) {
711 + $convert = 'avif';
712 + } elseif ( strpos( $size, static::WEBP_SUFFIX ) ) {
713 + $convert = 'webp';
714 + }
715 + }
716 +
678 717 if ( ! is_wp_error( $response ) ) {
679 718 // Resizing succeeded: optimize the file.
680 - $response = $file->optimize( [
681 - 'backup' => ! $response['backuped'] && $this->can_backup( $size ),
682 - 'backup_path' => $media->get_raw_backup_path(),
683 - 'backup_source' => 'full' === $thumb_size ? $media->get_original_path() : null,
684 - 'optimization_level' => $optimization_level,
685 - 'convert' => $webp ? 'webp' : '',
686 - 'keep_exif' => $this->can_keep_exif( $size ),
687 - 'context' => $media->get_context(),
688 - 'original_size' => $response['file_size'],
689 - ] );
719 + $response = $file->optimize(
720 + [
721 + 'backup' => ! $response['backuped'] && $this->can_backup( $size ),
722 + 'backup_path' => $media->get_raw_backup_path(),
723 + 'backup_source' => 'full' === $thumb_size ? $media->get_original_path() : null,
724 + 'optimization_level' => $optimization_level,
725 + 'convert' => $convert,
726 + 'keep_exif' => true,
727 + 'context' => $media->get_context(),
728 + 'original_size' => $response['file_size'],
729 + ]
730 + );
690 731
691 - $response = $this->compare_webp_file_size( [
692 - 'response' => $response,
693 - 'file' => $file,
694 - 'is_webp' => $webp,
695 - 'non_webp_thumb_size' => $thumb_size,
696 - 'non_webp_file_path' => $sizes[ $thumb_size ]['path'], // Don't use $path nor $file->get_path(), it may return the path to a temporary file.
697 - 'optimization_level' => $optimization_level,
698 - ] );
732 + $response = $this->compare_next_gen_file_size(
733 + [
734 + 'response' => $response,
735 + 'file' => $file,
736 + 'is_next_gen' => $next_gen,
737 + 'next_gen_format' => $convert,
738 + 'non_next_gen_thumb_size' => $thumb_size,
739 + 'non_next_gen_file_path' => $sizes[ $thumb_size ]['path'], // Don't use $path nor $file->get_path(), it may return the path to a temporary file.
740 + 'optimization_level' => $optimization_level,
741 + ]
742 + );
743 +
744 + if ( property_exists( $response, 'message' ) ) {
745 + $path_is_temp = false;
746 + if ( $path !== $sizes[ $thumb_size ]['path'] ) {
747 + $this->filesystem->delete( $path );
748 + }
749 + $path = $sizes[ $thumb_size ]['path'];
750 + }
699 751 }
700 752 }
701 753 }
702 754
@@ -704,10 +756,9 @@
704 756
705 757 /**
706 758 * Fires after optimizing a file.
707 759 *
708 - * @since 1.9
709 - * @author Grégory Viguier
760 + * @since 1.9
710 761 *
711 762 * @param ProcessInterface $process The optimization process instance.
712 763 * @param File $file The file instance.
713 764 * @param string $thumb_size The media size.
@@ -714,9 +765,9 @@
714 765 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
715 766 * @param bool $webp The image was supposed to be converted to WebP.
716 767 * @param bool $is_disabled Tell if this size is disabled from optimization.
717 768 */
718 - do_action( 'imagify_after_optimize_size', $this, $file, $thumb_size, $optimization_level, $webp, $is_disabled );
769 + do_action( 'imagify_after_optimize_size', $this, $file, $thumb_size, $optimization_level, $next_gen, $is_disabled );
719 770
720 771 if ( ! $path_is_temp ) {
721 772 return $data;
722 773 }
@@ -736,113 +787,111 @@
736 787 return $data;
737 788 }
738 789
739 790 /**
740 - * Compare the file size of a file and its WebP version: if the WebP version is heavier than the non-WebP file, delete it.
791 + * Compare the file size of a file and its Next-Gen version: if the Next-Gen version is heavier than the non-next-gen file, delete it.
741 792 *
742 - * @since 1.9.4
743 - * @access protected
744 - * @author Grégory Viguier
793 + * @since 2.2
745 794 *
746 - * @param array $args {
795 + * @param array $args {
747 796 * A list of mandatory arguments.
748 797 *
749 - * @type \sdtClass|\WP_Error $response Optimized image data. A \WP_Error object on error.
798 + * @type \sdtClass|WP_Error $response Optimized image data. A WP_Error object on error.
750 799 * @type File $file The File instance of the file currently being optimized.
751 - * @type bool $is_webp Tell if we're requesting a WebP file.
752 - * @type string $non_webp_thumb_size Name of the corresponding non-WebP thumbnail size. If we're not creating a WebP file, this corresponds to the current thumbnail size.
753 - * @type string $non_webp_file_path Path to the corresponding non-WebP file. If we're not creating a WebP file, this corresponds to the current file path.
800 + * @type bool $is_next_gen Tell if we're requesting a next-gen file.
801 + * @type string $non_next_gen_thumb_size Name of the corresponding non-next-gen thumbnail size. If we're not creating a Next-Gen file, this corresponds to the current thumbnail size.
802 + * @type string $non_next_gen_file_path Path to the corresponding non-next-gen file. If we're not creating a Next-Gen file, this corresponds to the current file path.
754 803 * @type string $optimization_level The optimization level.
755 804 * }
756 - * @return \sdtClass|\WP_Error Optimized image data. A \WP_Error object on error.
805 + *
806 + * @return \sdtClass|WP_Error Optimized image data. A WP_Error object on error.
757 807 */
758 - protected function compare_webp_file_size( $args ) {
759 - static $keep_large_webp;
808 + protected function compare_next_gen_file_size( $args ) {
809 + static $keep_large_next_gen;
760 810
761 - if ( ! isset( $keep_large_webp ) ) {
811 + if ( ! isset( $keep_large_next_gen ) ) {
762 812 /**
763 - * Allow to not store WebP images that are larger than their non-WebP version.
813 + * Allow to not store next-gen images that are larger than their non-next-gen version.
764 814 *
765 - * @since 1.9.4
766 - * @author Grégory Viguier
815 + * @since 1.9.4
767 816 *
768 - * @param bool $keep_large_webp Set to false if you prefer your visitors over your Pagespeed score. Default value is true.
817 + * @param bool $keep_large_next-gen Set to false if you prefer your visitors over your Pagespeed score. Default value is true.
769 818 */
770 - $keep_large_webp = apply_filters( 'imagify_keep_large_webp', true );
819 + $keep_large_next_gen = apply_filters( 'imagify_keep_large_next_gen', true );
771 820 }
772 821
773 - if ( $keep_large_webp || is_wp_error( $args['response'] ) || ! $args['file']->is_image() ) {
822 + if ( $keep_large_next_gen || is_wp_error( $args['response'] ) || ! $args['file']->is_image() ) {
774 823 return $args['response'];
775 824 }
776 825
777 826 // Optimization succeeded.
778 - if ( $args['is_webp'] ) {
827 + if ( ! property_exists( $args['response'], 'message' ) && $args['is_next_gen'] ) {
779 828 /**
780 - * We just created a WebP version:
781 - * Check if it is lighter than the (maybe optimized) non-WebP file.
829 + * We just created a next-gen version:
830 + * Check if it is lighter than the (maybe optimized) non-next-gen file.
782 831 */
783 - $data = $this->get_data()->get_size_data( $args['non_webp_thumb_size'] );
832 + $data = $this->get_data()->get_size_data( $args['non_next_gen_thumb_size'] );
784 833
785 834 if ( ! $data ) {
786 - // We haven’t tried to optimize the non-WebP size yet.
835 + // We haven’t tried to optimize the non-next-gen size yet.
787 836 return $args['response'];
788 837 }
789 838
790 839 if ( ! empty( $data['optimized_size'] ) ) {
791 - // The non-WebP size is optimized, we know the file size.
792 - $non_webp_file_size = $data['optimized_size'];
840 + // The non-next-gen size is optimized, we know the file size.
841 + $non_next_gen_file_size = $data['optimized_size'];
793 842 } else {
794 - // The non-WebP size is "already optimized" or "error": grab the file size directly from the file.
795 - $non_webp_file_size = $this->filesystem->size( $args['non_webp_file_path'] );
843 + // The non-next-gen size is "already optimized" or "error": grab the file size directly from the file.
844 + $non_next_gen_file_size = $this->filesystem->size( $args['non_next_gen_file_path'] );
796 845 }
797 846
798 - if ( ! $non_webp_file_size || $non_webp_file_size > $args['response']->new_size ) {
799 - // The new WebP file is lighter.
847 + if ( ! $non_next_gen_file_size || $non_next_gen_file_size > $args['response']->new_size ) {
848 + // The new next-gen file is lighter.
800 849 return $args['response'];
801 850 }
802 851
803 - // The new WebP file is heavier than the non-WebP file: delete it and return an error.
852 + // The new next-gen file is heavier than the non-next-gen file: delete it and return an error.
804 853 $this->filesystem->delete( $args['file']->get_path() );
805 854
806 - return new \WP_Error(
807 - 'webp_heavy',
855 + return new WP_Error(
856 + 'next_gen_heavy',
808 857 sprintf(
809 - /* translators: %s is a size name. */
810 - __( 'The WebP version of the size %s is heavier than its non-WebP version.', 'imagify' ),
811 - '<code>' . esc_html( $args['non_webp_thumb_size'] ) . '</code>'
858 + /* translators: %s is a size name. */
859 + __( 'The Next-Gen version of the size %s is heavier than its non-next-gen version.', 'imagify' ),
860 + '<code>' . esc_html( $args['non_next_gen_thumb_size'] ) . '</code>'
812 861 )
813 862 );
814 863 }
815 864
816 865 /**
817 - * We just created a non-WebP version:
818 - * Check if its WebP version file is lighter than this one.
866 + * We just created a non-next-gen version:
867 + * Check if its next-gen version file is lighter than this one.
819 868 */
820 - $webp_size = $args['non_webp_thumb_size'] . static::WEBP_SUFFIX;
821 - $webp_file_size = $this->get_data()->get_size_data( $webp_size, 'optimized_size' );
869 + $next_gen_size = $args['non_next_gen_thumb_size'] . $args['next_gen_format'];
870 + $next_gen_file_size = $this->get_data()->get_size_data( $next_gen_size, 'optimized_size' );
822 871
823 - if ( ! $webp_file_size || $webp_file_size < $args['response']->new_size ) {
824 - // The WebP file is lighter than this one.
872 + if ( property_exists( $args['response'], 'message' ) || ! $next_gen_file_size || $next_gen_file_size < $args['response']->new_size ) {
873 + // The next-gen file is lighter than this one.
825 874 return $args['response'];
826 875 }
827 876
828 - // The new optimized file is lighter than the WebP file: delete the WebP file and store an error.
829 - $webp_path = $args['file']->get_path_to_webp();
877 + // The new optimized file is lighter than the next-gen file: delete the next-gen file and store an error.
878 + $next_gen_path = $args['file']->get_path_to_nextgen( $args['next_gen_format'] );
830 879
831 - if ( $webp_path && $this->filesystem->is_writable( $webp_path ) ) {
832 - $this->filesystem->delete( $webp_path );
880 + if ( $next_gen_path && $this->filesystem->is_writable( $next_gen_path ) ) {
881 + $this->filesystem->delete( $next_gen_path );
833 882 }
834 883
835 - $webp_response = new \WP_Error(
836 - 'webp_heavy',
884 + $next_gen_response = new WP_Error(
885 + 'next_gen_heavy',
837 886 sprintf(
838 - /* translators: %s is a size name. */
839 - __( 'The WebP version of the size %s is heavier than its non-WebP version.', 'imagify' ),
840 - '<code>' . esc_html( $args['non_webp_thumb_size'] ) . '</code>'
887 + /* translators: %s is a size name. */
888 + __( 'The Next-Gen version of the size %s is heavier than its non-next-gen version.', 'imagify' ),
889 + '<code>' . esc_html( $args['non_next_gen_thumb_size'] ) . '</code>'
841 890 )
842 891 );
843 892
844 - $this->update_size_optimization_data( $webp_response, $webp_size, $args['optimization_level'] );
893 + $this->update_size_optimization_data( $next_gen_response, $next_gen_size, $args['optimization_level'] );
845 894
846 895 return $args['response'];
847 896 }
848 897
@@ -848,31 +897,29 @@
848 897
849 898 /**
850 899 * Restore the media files from the backup file.
851 900 *
852 - * @since 1.9
853 - * @access public
854 - * @author Grégory Viguier
901 + * @since 1.9
855 902 *
856 - * @return bool|WP_Error True on success. A \WP_Error instance on failure.
903 + * @return bool|WP_Error True on success. A WP_Error instance on failure.
857 904 */
858 905 public function restore() {
859 906 if ( ! $this->is_valid() ) {
860 - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
907 + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
861 908 }
862 909
863 910 $media = $this->get_media();
864 911
865 912 if ( ! $media->is_supported() ) {
866 - return new \WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
913 + return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
867 914 }
868 915
869 916 if ( ! $media->has_backup() ) {
870 - return new \WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) );
917 + return new WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) );
871 918 }
872 919
873 920 if ( $this->is_locked() ) {
874 - return new \WP_Error( 'media_locked', __( 'This media is already being processed.', 'imagify' ) );
921 + return new WP_Error( 'media_locked', __( 'This media is already being processed.', 'imagify' ) );
875 922 }
876 923
877 924 $this->lock( 'restoring' );
878 925
@@ -879,11 +926,10 @@
879 926 $backup_path = $media->get_backup_path();
880 927 $original_path = $media->get_raw_original_path();
881 928
882 929 if ( $backup_path === $original_path ) {
883 - // Uh?!
884 930 $this->unlock();
885 - return new \WP_Error( 'same_path', __( 'Image path and backup path are identical.', 'imagify' ) );
931 + return new WP_Error( 'same_path', __( 'Image path and backup path are identical.', 'imagify' ) );
886 932 }
887 933
888 934 $dest_dir = $this->filesystem->dir_path( $original_path );
889 935
@@ -894,9 +940,9 @@
894 940 $dest_file_is_writable = ! $this->filesystem->exists( $original_path ) || $this->filesystem->is_writable( $original_path );
895 941
896 942 if ( ! $dest_file_is_writable || ! $this->filesystem->is_writable( $dest_dir ) ) {
897 943 $this->unlock();
898 - return new \WP_Error( 'destination_not_writable', __( 'The image to replace is not writable.', 'imagify' ) );
944 + return new WP_Error( 'destination_not_writable', __( 'The image to replace is not writable.', 'imagify' ) );
899 945 }
900 946
901 947 // Get some data before doing anything.
902 948 $data = $this->get_data()->get_optimization_data();
@@ -903,14 +949,13 @@
903 949 $files = $media->get_media_files();
904 950
905 951 /**
906 952 * Fires before restoring a media.
907 - * Return a \WP_Error object to prevent the restoration.
953 + * Return a WP_Error object to prevent the restoration.
908 954 *
909 - * @since 1.9
910 - * @author Grégory Viguier
955 + * @since 1.9
911 956 *
912 - * @param null|\WP_Error $response Null by default. Return a \WP_Error object to prevent optimization.
957 + * @param null|WP_Error $response Null by default. Return a WP_Error object to prevent optimization.
913 958 * @param ProcessInterface $process Instance of this process.
914 959 */
915 960 $response = apply_filters( 'imagify_before_restore_media', null, $this );
916 961
@@ -919,9 +964,9 @@
919 964 $response = $this->filesystem->copy( $backup_path, $original_path, true );
920 965
921 966 if ( ! $response ) {
922 967 // Failure.
923 - $response = new \WP_Error( 'copy_failed', __( 'The backup file could not be copied over the optimized one.', 'imagify' ) );
968 + $response = new WP_Error( 'copy_failed', __( 'The backup file could not be copied over the optimized one.', 'imagify' ) );
924 969 } else {
925 970 // Backup successfully copied.
926 971 $this->filesystem->chmod_file( $original_path );
927 972
@@ -932,9 +977,9 @@
932 977 // Restore the original dimensions in the database.
933 978 $media->update_dimensions();
934 979
935 980 // Delete the WebP version.
936 - $this->delete_webp_file( $original_path );
981 + $this->delete_nextgen_file( $original_path, true );
937 982
938 983 // Restore the thumbnails.
939 984 $response = $this->restore_thumbnails();
940 985 }
@@ -943,10 +988,9 @@
943 988
944 989 /**
945 990 * Fires after restoring a media.
946 991 *
947 - * @since 1.9
948 - * @author Grégory Viguier
992 + * @since 1.9
949 993 *
950 994 * @param ProcessInterface $process Instance of this process.
951 995 * @param bool|WP_Error $response The result of the operation: true on success, a WP_Error object on failure.
952 996 * @param array $files The list of files, before restoring them.
@@ -961,40 +1005,31 @@
961 1005
962 1006 /**
963 1007 * Restore the thumbnails.
964 1008 *
965 - * @since 1.9
966 - * @access protected
967 - * @author Grégory Viguier
1009 + * @since 1.9
968 1010 *
969 - * @return bool|WP_Error True on success. A \WP_Error instance on failure.
1011 + * @return bool|WP_Error True on success. A WP_Error instance on failure.
970 1012 */
971 1013 protected function restore_thumbnails() {
972 1014 $media = $this->get_media();
973 1015
974 1016 /**
975 - * Delete the WebP versions.
1017 + * Delete the next-gen versions.
976 1018 * If the full size file and the original file are not the same, the full size is considered like a thumbnail.
977 - * In that case we must also delete the WebP file associated to the full size.
1019 + * In that case we must also delete the next-gen file associated to the full size.
978 1020 */
979 - $keep_full_webp = $media->get_raw_original_path() === $media->get_raw_fullsize_path();
980 - $this->delete_webp_files( $keep_full_webp );
1021 + $keep_full_next_gen = $media->get_raw_original_path() === $media->get_raw_fullsize_path();
1022 + $this->delete_nextgen_files( $keep_full_next_gen, true );
981 1023
982 1024 // Generate new thumbnails.
983 1025 return $media->generate_thumbnails();
984 1026 }
985 1027
986 -
987 - /** ----------------------------------------------------------------------------------------- */
988 - /** BACKUP FILE ============================================================================= */
989 - /** ----------------------------------------------------------------------------------------- */
990 -
991 1028 /**
992 1029 * Delete the backup file.
993 1030 *
994 - * @since 1.9
995 - * @access public
996 - * @author Grégory Viguier
1031 + * @since 1.9
997 1032 */
998 1033 public function delete_backup() {
999 1034 if ( ! $this->is_valid() ) {
1000 1035 return;
@@ -1003,32 +1038,31 @@
1003 1038 $backup_path = $this->get_media()->get_backup_path();
1004 1039
1005 1040 if ( $backup_path ) {
1006 1041 $this->filesystem->delete( $backup_path );
1042 +
1043 + // Check for the -scaled version in the backup.
1044 + $scaled_backup_path = preg_replace( '/(\.)([^\.]+)$/', '-scaled.$2', $backup_path );
1045 + if ( $this->filesystem->exists( $scaled_backup_path ) ) {
1046 + // Delete the -scaled version from the backup.
1047 + $this->filesystem->delete( $scaled_backup_path );
1048 + }
1007 1049 }
1008 1050 }
1009 1051
1010 -
1011 - /** ----------------------------------------------------------------------------------------- */
1012 - /** TEMPORARY COPY OF A SIZE FILE =========================================================== */
1013 - /** ----------------------------------------------------------------------------------------- */
1014 -
1015 1052 /**
1016 - * If we need to create a WebP version, we must create it from an unoptimized image.
1017 - * The full size is always optimized before the WebP version creation, and in some cases it’s the same for the thumbnails.
1053 + * Create a temporary copy of a size file.
1054 + *
1055 + * If we need to create a next-gen version, we must create it from an unoptimized image.
1056 + * The full size is always optimized before the next-gen version creation, and in some cases it’s the same for the thumbnails.
1018 1057 * Then we use the backup file to create temporary files.
1019 - */
1020 -
1021 - /**
1022 - * Create a temporary copy of a size file.
1023 1058 *
1024 - * @since 1.9
1025 - * @access protected
1026 - * @author Grégory Viguier
1059 + * @since 1.9
1027 1060 *
1028 - * @param string $size The image size name.
1029 - * @param array $sizes A list of thumbnail sizes being optimized.
1030 - * @return bool True if the file exists/is created. False on failure.
1061 + * @param string $size The image size name.
1062 + * @param array $sizes A list of thumbnail sizes being optimized.
1063 + *
1064 + * @return bool True if the file exists/is created. False on failure.
1031 1065 */
1032 1066 protected function create_temporary_copy( $size, $sizes = null ) {
1033 1067 $media = $this->get_media();
1034 1068
@@ -1079,11 +1113,20 @@
1079 1113 if ( ! $copied ) {
1080 1114 return false;
1081 1115 }
1082 1116
1117 + /**
1118 + * The backup file may be the original, un-rotated JPEG that WordPress auto-rotated on
1119 + * upload (WordPress resets the orientation on the rotated file, but keeps the original
1120 + * orientation in the backup). Correct the orientation of this disposable temporary copy
1121 + * so that a Next-Gen version (or a thumbnail) generated from it isn't mis-oriented.
1122 + * The backup file itself is never touched.
1123 + */
1124 + $tmp_file->maybe_correct_exif_orientation();
1125 +
1083 1126 if ( 'full' === $size ) {
1084 1127 /**
1085 - * We create a copy of the backup to be able to create a WebP version from it.
1128 + * We create a copy of the backup to be able to create a next-gen version from it.
1086 1129 * That means the optimization process will resize the file if needed, so there is nothing more to do here.
1087 1130 */
1088 1131 return true;
1089 1132 }
@@ -1105,10 +1148,9 @@
1105 1148 if ( ! isset( $size_data['crop'] ) ) {
1106 1149 /**
1107 1150 * In case of a dynamic thumbnail we don’t know if the image must be croped or resized.
1108 1151 *
1109 - * @since 1.9
1110 - * @author Grégory Viguier
1152 + * @since 1.9
1111 1153 *
1112 1154 * @param bool $crop True to crop the thumbnail, false to resize. Null by default.
1113 1155 * @param string $size Name of the thumbnail size.
1114 1156 * @param array $size_data Data of the thumbnail being processed. Contains at least 'width', 'height', and 'path'.
@@ -1134,9 +1176,8 @@
1134 1176
1135 1177 $thumb_dimensions = $this->filesystem->get_image_size( $size_data['path'] );
1136 1178
1137 1179 if ( ! $thumb_dimensions || ! $thumb_dimensions['width'] || ! $thumb_dimensions['height'] ) {
1138 - // ( ; Д ; )
1139 1180 return false;
1140 1181 }
1141 1182
1142 1183 // Compare dimensions.
@@ -1145,15 +1186,17 @@
1145 1186 $size_data['crop'] = abs( $thumb_dimensions['height'] - $new_height ) > 1;
1146 1187 }
1147 1188 }
1148 1189
1149 - $resized = $tmp_file->create_thumbnail( [
1150 - 'path' => $tmp_path,
1151 - 'width' => $size_data['width'],
1152 - 'height' => $size_data['height'],
1153 - 'crop' => $size_data['crop'],
1154 - 'adjust_filename' => false,
1155 - ] );
1190 + $resized = $tmp_file->create_thumbnail(
1191 + [
1192 + 'path' => $tmp_path,
1193 + 'width' => $size_data['width'],
1194 + 'height' => $size_data['height'],
1195 + 'crop' => $size_data['crop'],
1196 + 'adjust_filename' => false,
1197 + ]
1198 + );
1156 1199
1157 1200 if ( is_wp_error( $resized ) ) {
1158 1201 return false;
1159 1202 }
@@ -1170,14 +1213,13 @@
1170 1213
1171 1214 /**
1172 1215 * Get the path to a temporary copy of a size file.
1173 1216 *
1174 - * @since 1.9
1175 - * @access protected
1176 - * @author Grégory Viguier
1217 + * @since 1.9
1177 1218 *
1178 - * @param string $size The image size name.
1179 - * @param array $sizes A list of thumbnail sizes being optimized.
1219 + * @param string $size The image size name.
1220 + * @param array $sizes A list of thumbnail sizes being optimized.
1221 + *
1180 1222 * @return string|bool An image path. False on failure.
1181 1223 */
1182 1224 protected function get_temporary_copy_path( $size, $sizes = null ) {
1183 1225 if ( 'full' === $size ) {
@@ -1202,23 +1244,17 @@
1202 1244
1203 1245 return $info['dir_path'] . $info['file_base'] . static::TMP_SUFFIX . '.' . $info['extension'];
1204 1246 }
1205 1247
1206 -
1207 - /** ----------------------------------------------------------------------------------------- */
1208 - /** RESIZE FILE ============================================================================= */
1209 - /** ----------------------------------------------------------------------------------------- */
1210 -
1211 1248 /**
1212 1249 * Maybe resize an image.
1213 1250 *
1214 - * @since 1.9
1215 - * @access public
1216 - * @author Grégory Viguier
1251 + * @since 1.9
1217 1252 *
1218 - * @param string $size The size name.
1219 - * @param File $file A File instance.
1220 - * @return array|WP_Error A \WP_Error instance on failure, an array on success as follow: {
1253 + * @param string $size The size name.
1254 + * @param File $file A File instance.
1255 + *
1256 + * @return array|WP_Error A WP_Error instance on failure, an array on success as follow: {
1221 1257 * @type bool $resized True when the image has been resized.
1222 1258 * @type bool $backuped True when the image has been backuped.
1223 1259 * @type int $file_size The file size in bytes.
1224 1260 * }
@@ -1232,22 +1268,16 @@
1232 1268 'file_size' => 0,
1233 1269 ];
1234 1270 }
1235 1271
1236 - $media = $this->get_media();
1237 - $dimensions = $media->get_dimensions();
1272 + $dimensions = $file->get_dimensions();
1238 1273
1239 1274 if ( ! $dimensions['width'] ) {
1240 - // The dimensions don't seem to be in the database anymore: try to get them directly from the file.
1241 - $dimensions = $file->get_dimensions();
1242 - }
1243 -
1244 - if ( ! $dimensions['width'] ) {
1245 1275 // Could not get the image dimensions.
1246 - return new \WP_Error(
1276 + return new WP_Error(
1247 1277 'no_dimensions',
1248 1278 sprintf(
1249 - /* translators: %s is an error message. */
1279 + /* translators: %s is an error message. */
1250 1280 __( 'Resizing failed: %s', 'imagify' ),
1251 1281 __( 'Imagify could not get the image dimensions.', 'imagify' )
1252 1282 )
1253 1283 );
@@ -1252,8 +1282,9 @@
1252 1282 )
1253 1283 );
1254 1284 }
1255 1285
1286 + $media = $this->get_media();
1256 1287 $resize_width = $media->get_context_instance()->get_resizing_threshold();
1257 1288
1258 1289 if ( $resize_width >= $dimensions['width'] ) {
1259 1290 // No need to resize.
@@ -1267,12 +1298,12 @@
1267 1298 $resized_path = $file->resize( $dimensions, $resize_width );
1268 1299
1269 1300 if ( is_wp_error( $resized_path ) ) {
1270 1301 // The resizement failed.
1271 - return new \WP_Error(
1302 + return new WP_Error(
1272 1303 'resize_failure',
1273 1304 sprintf(
1274 - /* translators: %s is an error message. */
1305 + /* translators: %s is an error message. */
1275 1306 __( 'Resizing failed: %s', 'imagify' ),
1276 1307 $resized_path->get_error_message()
1277 1308 )
1278 1309 );
@@ -1283,12 +1314,12 @@
1283 1314 $backuped = $file->backup( $media->get_raw_backup_path(), $source );
1284 1315
1285 1316 if ( is_wp_error( $backuped ) ) {
1286 1317 // The backup failed.
1287 - return new \WP_Error(
1318 + return new WP_Error(
1288 1319 'backup_failure',
1289 1320 sprintf(
1290 - /* translators: %s is an error message. */
1321 + /* translators: %s is an error message. */
1291 1322 __( 'Backup failed: %s', 'imagify' ),
1292 1323 $backuped->get_error_message()
1293 1324 )
1294 1325 );
@@ -1301,9 +1332,9 @@
1301 1332 $resized = $this->filesystem->move( $resized_path, $file->get_path(), true );
1302 1333
1303 1334 if ( ! $resized ) {
1304 1335 // The resizement failed.
1305 - return new \WP_Error(
1336 + return new WP_Error(
1306 1337 'resize_move_failure',
1307 1338 __( 'The image could not be replaced by the resized one.', 'imagify' )
1308 1339 );
1309 1340 }
@@ -1320,14 +1351,13 @@
1320 1351
1321 1352 /**
1322 1353 * Tell if a size should be resized.
1323 1354 *
1324 - * @since 1.9
1325 - * @access protected
1326 - * @author Grégory Viguier
1355 + * @since 1.9
1327 1356 *
1328 - * @param string $size The size name.
1329 - * @param File $file A File instance.
1357 + * @param string $size The size name.
1358 + * @param File $file A File instance.
1359 + *
1330 1360 * @return bool
1331 1361 */
1332 1362 protected function can_resize( $size, $file ) {
1333 1363 if ( ! $this->is_valid() ) {
@@ -1333,10 +1363,18 @@
1333 1363 if ( ! $this->is_valid() ) {
1334 1364 return false;
1335 1365 }
1336 1366
1337 - if ( 'full' !== $size && 'full' . static::WEBP_SUFFIX !== $size ) {
1338 - // We resize only the main file and its WebP version.
1367 + if (
1368 + 'full' !== $size
1369 + &&
1370 + (
1371 + 'full' . static::WEBP_SUFFIX !== $size
1372 + ||
1373 + 'full' . static::AVIF_SUFFIX !== $size
1374 + )
1375 + ) {
1376 + // We resize only the main file and its next-gen version.
1339 1377 return false;
1340 1378 }
1341 1379
1342 1380 if ( ! $file->is_image() ) {
@@ -1348,13 +1386,12 @@
1348 1386
1349 1387 /**
1350 1388 * Tell if a size should be backuped.
1351 1389 *
1352 - * @since 1.9
1353 - * @access protected
1354 - * @author Grégory Viguier
1390 + * @since 1.9
1355 1391 *
1356 - * @param string $size The size name.
1392 + * @param string $size The size name.
1393 + *
1357 1394 * @return bool
1358 1395 */
1359 1396 protected function can_backup( $size ) {
1360 1397 if ( ! $this->is_valid() ) {
@@ -1369,112 +1406,41 @@
1369 1406 return $this->get_media()->get_context_instance()->can_backup();
1370 1407 }
1371 1408
1372 1409 /**
1373 - * Tell if a size should keep exif.
1410 + * Get mime type
1374 1411 *
1375 - * @since 1.9
1376 - * @access protected
1377 - * @author Grégory Viguier
1378 - *
1379 - * @param string $size The size name.
1380 - * @return bool
1412 + * @param string $format nextgen image format.
1381 1413 */
1382 - protected function can_keep_exif( $size ) {
1383 - if ( ! $this->is_valid() ) {
1384 - return false;
1385 - }
1386 -
1387 - if ( 'full' !== $size && 'full' . static::WEBP_SUFFIX !== $size ) {
1388 - // We keep exif only on the main file and its WebP version.
1389 - return false;
1390 - }
1391 -
1392 - return $this->get_media()->get_context_instance()->can_keep_exif();
1393 - }
1394 -
1395 -
1396 - /** ----------------------------------------------------------------------------------------- */
1397 - /** WEBP ==================================================================================== */
1398 - /** ----------------------------------------------------------------------------------------- */
1399 -
1400 - /**
1401 - * Generate WebP images if they are missing.
1402 - *
1403 - * @since 1.9
1404 - * @access public
1405 - * @author Grégory Viguier
1406 - *
1407 - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
1408 - */
1409 - public function generate_webp_versions() {
1410 - if ( ! $this->is_valid() ) {
1411 - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
1412 - }
1413 -
1414 - $media = $this->get_media();
1415 -
1416 - if ( ! $media->is_image() ) {
1417 - return new \WP_Error( 'no_webp', __( 'This media is not an image and cannot be converted to WebP format.', 'imagify' ) );
1418 - }
1419 -
1420 - if ( ! $media->has_backup() ) {
1421 - return new \WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) );
1422 - }
1423 -
1424 - $data = $this->get_data();
1425 -
1426 - if ( ! $data->is_optimized() && ! $data->is_already_optimized() ) {
1427 - return new \WP_Error( 'not_optimized', __( 'This media has not been optimized by Imagify yet.', 'imagify' ) );
1428 - }
1429 -
1430 - if ( $this->has_webp() ) {
1431 - return new \WP_Error( 'has_webp', __( 'This media already has WebP versions.', 'imagify' ) );
1432 - }
1433 -
1434 - $files = $media->get_media_files();
1435 - $sizes = [];
1436 - $args = [
1437 - 'hook_suffix' => 'generate_webp_versions',
1414 + private function get_mime_type( $format ) {
1415 + $mime_types = [
1416 + 'avif' => 'image/avif',
1417 + 'webp' => 'image/webp',
1438 1418 ];
1439 1419
1440 - foreach ( $files as $size_name => $file ) {
1441 - if ( 'image/webp' !== $files[ $size_name ]['mime-type'] ) {
1442 - array_unshift( $sizes, $size_name . static::WEBP_SUFFIX );
1443 - }
1444 - }
1445 -
1446 - if ( ! $sizes ) {
1447 - return new \WP_Error( 'no_sizes', __( 'This media does not have files that can be converted to WebP format.', 'imagify' ) );
1448 - }
1449 -
1450 - $optimization_level = $data->get_optimization_level();
1451 -
1452 - // Optimize.
1453 - return $this->optimize_sizes( $sizes, $optimization_level, $args );
1420 + return isset( $mime_types[ $format ] ) ? $mime_types[ $format ] : false;
1454 1421 }
1455 1422
1456 1423 /**
1457 - * Delete the WebP images.
1424 + * Delete the next gen format images.
1458 1425 * This doesn't delete the related optimization data.
1459 1426 *
1460 - * @since 1.9
1461 - * @since 1.9.6 Return WP_Error or true.
1462 - * @access public
1463 - * @author Grégory Viguier
1427 + * @since 2.2
1464 1428 *
1465 - * @param bool $keep_full Set to true to keep the full size.
1466 - * @return bool|\WP_Error True on success. A \WP_Error object on failure.
1429 + * @param bool $keep_full Set to true to keep the full size.
1430 + * @param bool $all_next_gen True: will delete every next-gen format. False: will delete only the current enabled format.
1431 + *
1432 + * @return bool|WP_Error True on success. A WP_Error object on failure.
1467 1433 */
1468 - public function delete_webp_files( $keep_full = false ) {
1434 + public function delete_nextgen_files( $keep_full = false, $all_next_gen = false ) {
1469 1435 if ( ! $this->is_valid() ) {
1470 - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
1436 + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
1471 1437 }
1472 1438
1473 1439 $media = $this->get_media();
1474 1440
1475 1441 if ( ! $media->is_image() ) {
1476 - return new \WP_Error( 'media_not_an_image', __( 'This media is not an image.', 'imagify' ) );
1442 + return new WP_Error( 'media_not_an_image', __( 'This media is not an image.', 'imagify' ) );
1477 1443 }
1478 1444
1479 1445 $files = $media->get_media_files();
1480 1446
@@ -1489,9 +1455,9 @@
1489 1455 $error_count = 0;
1490 1456
1491 1457 foreach ( $files as $file ) {
1492 1458 if ( 0 === strpos( $file['mime-type'], 'image/' ) ) {
1493 - $deleted = $this->delete_webp_file( $file['path'] );
1459 + $deleted = $this->delete_nextgen_file( $file['path'], $all_next_gen );
1494 1460
1495 1461 if ( is_wp_error( $deleted ) ) {
1496 1462 ++$error_count;
1497 1463 }
@@ -1498,12 +1464,12 @@
1498 1464 }
1499 1465 }
1500 1466
1501 1467 if ( $error_count ) {
1502 - return new \WP_Error(
1468 + return new WP_Error(
1503 1469 'files_not_deleted',
1504 1470 sprintf(
1505 - /* translators: %s is a formatted number, don’t use %d. */
1471 + /* translators: %s is a formatted number, don’t use %d. */
1506 1472 _n( '%s file could not be deleted.', '%s files could not be deleted.', $error_count, 'imagify' ),
1507 1473 number_format_i18n( $error_count )
1508 1474 )
1509 1475 );
@@ -1512,66 +1478,88 @@
1512 1478 return true;
1513 1479 }
1514 1480
1515 1481 /**
1516 - * Delete a WebP image, given its non-WebP version's path.
1482 + * Delete a next gen format image, given its non-next-gen version's path.
1517 1483 * This doesn't delete the related optimization data.
1518 1484 *
1519 - * @since 1.9
1520 - * @since 1.9.6 Return WP_Error or true.
1521 - * @access protected
1522 - * @author Grégory Viguier
1485 + * @since 2.2
1523 1486 *
1524 - * @param string $file_path Path to the non-WebP file.
1525 - * @return bool|\WP_Error True on success. A \WP_Error object on failure.
1487 + * @param string $file_path Path to the non-next-gen file.
1488 + * @param bool $all_next_gen True: will delete every next-gen format. False: will delete only the current enabled format.
1489 + *
1490 + * @return void|WP_Error A \WP_Error object on failure.
1526 1491 */
1527 - protected function delete_webp_file( $file_path ) {
1492 + protected function delete_nextgen_file( $file_path, $all_next_gen = false ) {
1528 1493 if ( ! $file_path ) {
1529 - return new \WP_Error( 'no_path', __( 'Path to non-WebP file not provided.', 'imagify' ) );
1494 + return new WP_Error( 'no_path', __( 'Path to non-next-gen file not provided.', 'imagify' ) );
1530 1495 }
1531 1496
1532 - $webp_file = new File( $file_path );
1533 - $webp_path = $webp_file->get_path_to_webp();
1497 + $next_gen_file = new File( $file_path );
1498 + $formats = $this->extensions;
1534 1499
1535 - if ( ! $webp_path ) {
1536 - return new \WP_Error( 'no_webp_path', __( 'Could not get the path to the WebP file.', 'imagify' ) );
1500 + if ( ! $all_next_gen ) {
1501 + $formats = imagify_nextgen_images_formats();
1537 1502 }
1503 + // Delete next-gen images.
1504 + foreach ( $formats as $extension ) {
1505 + $path = $next_gen_file->get_path_to_nextgen( $extension );
1538 1506
1539 - if ( ! $this->filesystem->exists( $webp_path ) ) {
1507 + if ( ! $path ) {
1508 + continue;
1509 + }
1510 +
1511 + $this->delete_file( $path );
1512 + }
1513 + }
1514 +
1515 + /**
1516 + * Delete a next gen format image, given its non-next-gen version's path.
1517 + *
1518 + * @param string $next_gen_path Path to the non-next-gen file.
1519 + *
1520 + * @return bool|WP_Error True on success. A WP_Error object on failure.
1521 + */
1522 + protected function delete_file( string $next_gen_path ) {
1523 + if ( empty( $next_gen_path ) ) {
1524 + return new WP_Error( 'no_$next_gen_path', __( 'Could not get the path to the Next-Gen format file.', 'imagify' ) );
1525 + }
1526 +
1527 + if ( ! $this->filesystem->exists( $next_gen_path ) ) {
1540 1528 return true;
1541 1529 }
1542 1530
1543 - if ( ! $this->filesystem->is_writable( $webp_path ) ) {
1544 - return new \WP_Error(
1531 + if ( ! $this->filesystem->is_writable( $next_gen_path ) ) {
1532 + return new WP_Error(
1545 1533 'file_not_writable',
1546 1534 sprintf(
1547 - /* translators: %s is a file path. */
1535 + /* translators: %s is a file path. */
1548 1536 __( 'The file %s does not seem to be writable.', 'imagify' ),
1549 - '<code>' . esc_html( $this->filesystem->make_path_relative( $webp_path ) ) . '</code>'
1537 + '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>'
1550 1538 )
1551 1539 );
1552 1540 }
1553 1541
1554 - if ( ! $this->filesystem->is_file( $webp_path ) ) {
1555 - return new \WP_Error(
1542 + if ( ! $this->filesystem->is_file( $next_gen_path ) ) {
1543 + return new WP_Error(
1556 1544 'not_a_file',
1557 1545 sprintf(
1558 - /* translators: %s is a file path. */
1546 + /* translators: %s is a file path. */
1559 1547 __( 'This does not seem to be a file: %s.', 'imagify' ),
1560 - '<code>' . esc_html( $this->filesystem->make_path_relative( $webp_path ) ) . '</code>'
1548 + '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>'
1561 1549 )
1562 1550 );
1563 1551 }
1564 1552
1565 - $deleted = $this->filesystem->delete( $webp_path, false, 'f' );
1553 + $deleted = $this->filesystem->delete( $next_gen_path, false, 'f' );
1566 1554
1567 1555 if ( ! $deleted ) {
1568 - return new \WP_Error(
1556 + return new WP_Error(
1569 1557 'file_not_deleted',
1570 1558 sprintf(
1571 - /* translators: %s is a file path. */
1559 + /* translators: %s is a file path. */
1572 1560 __( 'The file %s could not be deleted.', 'imagify' ),
1573 - '<code>' . esc_html( $this->filesystem->make_path_relative( $webp_path ) ) . '</code>'
1561 + '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>'
1574 1562 )
1575 1563 );
1576 1564 }
1577 1565
@@ -1578,26 +1566,37 @@
1578 1566 return true;
1579 1567 }
1580 1568
1581 1569 /**
1582 - * Tell if a thumbnail size is an "Imagify WebP" size.
1570 + * Gives the next-gen image format we are processing.
1583 1571 *
1584 - * @since 1.9
1585 - * @access public
1586 - * @author Grégory Viguier
1572 + * @return string Current format we are targeting.
1573 + */
1574 + public function get_current_format() {
1575 + $format = get_imagify_option( 'optimization_format' );
1576 +
1577 + return ( 'avif' === $format ) ? static::AVIF_SUFFIX : static::WEBP_SUFFIX;
1578 + }
1579 +
1580 + /**
1581 + * Tell if a thumbnail size is an "Imagify Next-Gen" size.
1587 1582 *
1588 - * @param string $size_name The size name.
1589 - * @return string|bool The unsuffixed name of the size if WebP. False if not WebP.
1583 + * @since 1.9
1584 + * @since 2.2 addition of the format parameter.
1585 + *
1586 + * @param string $size_name The size name.
1587 + *
1588 + * @return string|bool The unsuffixed name of the size if next-gen. False if not next-gen.
1590 1589 */
1591 - public function is_size_webp( $size_name ) {
1592 - static $suffix;
1590 + public function is_size_next_gen( $size_name ) {
1591 + $formats = imagify_nextgen_images_formats();
1593 1592
1594 - if ( ! isset( $suffix ) ) {
1595 - $suffix = preg_quote( static::WEBP_SUFFIX, '/' );
1596 - }
1593 + foreach ( $formats as $format ) {
1594 + $suffix = preg_quote( $this->get_suffix_from_format( $format ), '/' );
1597 1595
1598 - if ( preg_match( '/^(?<size>.+)' . $suffix . '$/', $size_name, $matches ) ) {
1599 - return $matches['size'];
1596 + if ( preg_match( '/^(?<size>.+)' . $suffix . '$/', (string) $size_name, $matches ) ) {
1597 + return $matches['size'];
1598 + }
1600 1599 }
1601 1600
1602 1601 return false;
1603 1602 }
@@ -1602,17 +1601,46 @@
1602 1601 return false;
1603 1602 }
1604 1603
1605 1604 /**
1606 - * Tell if the media has WebP versions.
1605 + * Tell if the next-gen file for a size is missing from disk.
1607 1606 *
1608 - * @since 1.9
1609 - * @access public
1610 - * @author Grégory Viguier
1607 + * @since 2.3.2
1611 1608 *
1609 + * @param string $size The (next-gen) size name, e.g. "thumbnail@imagify-avif".
1610 + * @param string $original_path Path to the non-next-gen version of that size.
1612 1611 * @return bool
1613 1612 */
1614 - public function has_webp() {
1613 + protected function is_next_gen_file_missing( string $size, string $original_path ): bool {
1614 + $format = strpos( $size, static::AVIF_SUFFIX ) ? 'avif' : 'webp';
1615 + $next_gen_path = imagify_path_to_nextgen( $original_path, $format );
1616 +
1617 + return ! $next_gen_path || ! $this->filesystem->exists( $next_gen_path );
1618 + }
1619 +
1620 + /**
1621 + * Get suffix from format.
1622 + *
1623 + * @param string $format Format extension of next-gen image.
1624 + * @return string
1625 + */
1626 + private function get_suffix_from_format( string $format ): string {
1627 + $suffixes = [
1628 + 'avif' => static::AVIF_SUFFIX,
1629 + 'webp' => static::WEBP_SUFFIX,
1630 + ];
1631 +
1632 + return $suffixes[ $format ];
1633 + }
1634 +
1635 + /**
1636 + * Tell if the media has a next gen format.
1637 + *
1638 + * @since 2.2
1639 + *
1640 + * @return bool
1641 + */
1642 + public function has_next_gen() {
1615 1643 if ( ! $this->is_valid() ) {
1616 1644 return false;
1617 1645 }
1618 1646
@@ -1625,9 +1653,9 @@
1625 1653 if ( empty( $data['sizes'] ) ) {
1626 1654 return false;
1627 1655 }
1628 1656
1629 - $needle = static::WEBP_SUFFIX . '";a:4:{s:7:"success";b:1;';
1657 + $needle = $this->format . '";a:4:{s:7:"success";b:1;';
1630 1658 $data = maybe_serialize( $data['sizes'] );
1631 1659
1632 1660 return is_string( $data ) && strpos( $data, $needle );
1633 1661 }
@@ -1632,34 +1660,76 @@
1632 1660 return is_string( $data ) && strpos( $data, $needle );
1633 1661 }
1634 1662
1635 1663 /**
1636 - * Tell if a WebP version can be created for the given file.
1664 + * Tell if the media has all Next-Gen versions.
1665 + *
1666 + * @return bool
1667 + */
1668 + public function is_full_next_gen() {
1669 + if ( ! $this->is_valid() ) {
1670 + return false;
1671 + }
1672 +
1673 + if ( ! $this->get_media()->is_image() ) {
1674 + return false;
1675 + }
1676 +
1677 + $data = $this->get_data()->get_optimization_data();
1678 +
1679 + $sizes = $data['sizes'];
1680 +
1681 + if ( empty( $sizes ) ) {
1682 + return false;
1683 + }
1684 +
1685 + $keys = array_keys( $sizes );
1686 +
1687 + $non_next_gen_keys = array_values(
1688 + array_filter(
1689 + $keys,
1690 + function ( $key ) {
1691 + return strpos( (string) $key, $this->format ) === false;
1692 + }
1693 + )
1694 + );
1695 +
1696 + return array_reduce(
1697 + $non_next_gen_keys,
1698 + function ( $is_fully, $key ) use ( $sizes ) {
1699 + return key_exists( $key . $this->format, $sizes ) && $is_fully;
1700 + },
1701 + true
1702 + );
1703 + }
1704 +
1705 + /**
1706 + * Tell if a Next-Gen version can be created for the given file.
1637 1707 * Make sure the file is an image before using this method.
1638 1708 *
1639 - * @since 1.9.5
1640 - * @access public
1641 - * @author Grégory Viguier
1709 + * @since 1.9.5
1642 1710 *
1643 1711 * @param string $file_path Path to the file.
1712 + *
1644 1713 * @return bool
1645 1714 */
1646 - public function can_create_webp_version( $file_path ) {
1715 + public function can_create_next_gen_version( $file_path ) {
1647 1716 if ( ! $file_path ) {
1648 1717 return false;
1649 1718 }
1650 1719
1720 + $can = apply_filters_deprecated( 'imagify_pre_can_create_webp_version', [ null, $file_path ], '2.2', 'imagify_pre_can_create_next_gen_version' );
1721 +
1651 1722 /**
1652 - * Tell if a WebP version can be created for the given file.
1723 + * Tell if a next-gen version can be created for the given file.
1653 1724 * The file is an image.
1654 1725 *
1655 - * @since 1.9.5
1656 - * @author Grégory Viguier
1726 + * @since 1.9.5
1657 1727 *
1658 - * @param bool $can True to create a WebP version, false otherwise. Null by default.
1728 + * @param bool $can True to create a next-gen version, false otherwise. Null by default.
1659 1729 * @param string $file_path Path to the file.
1660 1730 */
1661 - $can = apply_filters( 'imagify_pre_can_create_webp_version', null, $file_path );
1731 + $can = apply_filters( 'imagify_pre_can_create_next_gen_version', $can, $file_path );
1662 1732
1663 1733 if ( isset( $can ) ) {
1664 1734 return (bool) $can;
1665 1735 }
@@ -1674,19 +1744,78 @@
1674 1744 // At this point $is_animated_gif is null, which means the file cannot be read (yet).
1675 1745 return true;
1676 1746 }
1677 1747
1748 + /**
1749 + * Generate next-gen images if they are missing.
1750 + *
1751 + * @since 1.9
1752 + *
1753 + * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure.
1754 + */
1755 + public function generate_nextgen_versions() {
1756 + if ( ! $this->is_valid() ) {
1757 + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
1758 + }
1678 1759
1679 - /** ----------------------------------------------------------------------------------------- */
1680 - /** PROCESS STATUS ========================================================================== */
1681 - /** ----------------------------------------------------------------------------------------- */
1760 + $media = $this->get_media();
1682 1761
1762 + if ( ! $media->is_image() ) {
1763 + return new WP_Error( 'no_next_gen', __( 'This media is not an image and cannot be converted to next-gen format.', 'imagify' ) );
1764 + }
1765 +
1766 + if ( ! $media->has_backup() ) {
1767 + return new WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) );
1768 + }
1769 +
1770 + $data = $this->get_data();
1771 +
1772 + if ( ! $data->is_optimized() && ! $data->is_already_optimized() ) {
1773 + return new WP_Error( 'not_optimized', __( 'This media has not been optimized by Imagify yet.', 'imagify' ) );
1774 + }
1775 +
1776 + if ( $this->has_next_gen() ) {
1777 + return new WP_Error( 'has_next_gen', __( 'This media already has next-gen versions.', 'imagify' ) );
1778 + }
1779 +
1780 + $files = $media->get_media_files();
1781 + $sizes = [];
1782 + $args = [
1783 + 'hook_suffix' => 'generate_nextgen_versions',
1784 + ];
1785 +
1786 + foreach ( $files as $size_name => $file ) {
1787 + $formats = imagify_nextgen_images_formats();
1788 +
1789 + foreach ( $formats as $format ) {
1790 + if ( 'avif' === $format ) {
1791 + $format_suffix = static::AVIF_SUFFIX;
1792 + } elseif ( 'webp' === $format ) {
1793 + $format_suffix = static::WEBP_SUFFIX;
1794 + }
1795 +
1796 + if ( $this->get_mime_type( $format ) === $files[ $size_name ]['mime-type'] ) {
1797 + continue;
1798 + }
1799 +
1800 + array_unshift( $sizes, $size_name . $format_suffix );
1801 + }
1802 + }
1803 +
1804 + if ( ! $sizes ) {
1805 + return new WP_Error( 'no_sizes', __( 'This media does not have files that can be converted to next-gen format.', 'imagify' ) );
1806 + }
1807 +
1808 + $optimization_level = $data->get_optimization_level();
1809 +
1810 + // Optimize.
1811 + return $this->optimize_sizes( $sizes, $optimization_level, $args );
1812 + }
1813 +
1683 1814 /**
1684 1815 * Tell if a process is running for this media.
1685 1816 *
1686 - * @since 1.9
1687 - * @access public
1688 - * @author Grégory Viguier
1817 + * @since 1.9
1689 1818 *
1690 1819 * @return string|bool The action if locked ('optimizing' or 'restoring'). False if not locked.
1691 1820 */
1692 1821 public function is_locked() {
@@ -1708,11 +1837,9 @@
1708 1837
1709 1838 /**
1710 1839 * Set the running status to "running" for 10 minutes.
1711 1840 *
1712 - * @since 1.9
1713 - * @access public
1714 - * @author Grégory Viguier
1841 + * @since 1.9
1715 1842 *
1716 1843 * @param string $action The action performed behind this lock: 'optimizing' or 'restoring'.
1717 1844 */
1718 1845 public function lock( $action = 'optimizing' ) {
@@ -1731,11 +1858,9 @@
1731 1858
1732 1859 /**
1733 1860 * Unset the running status.
1734 1861 *
1735 - * @since 1.9
1736 - * @access public
1737 - * @author Grégory Viguier
1862 + * @since 1.9
1738 1863 */
1739 1864 public function unlock() {
1740 1865 $name = $this->get_lock_name();
1741 1866
@@ -1750,11 +1875,9 @@
1750 1875
1751 1876 /**
1752 1877 * Get the name of the transient that stores the lock status.
1753 1878 *
1754 - * @since 1.9
1755 - * @access protected
1756 - * @author Grégory Viguier
1879 + * @since 1.9
1757 1880 *
1758 1881 * @return string|bool The name on success. False on failure.
1759 1882 */
1760 1883 protected function get_lock_name() {
@@ -1773,11 +1896,9 @@
1773 1896
1774 1897 /**
1775 1898 * Validate the lock action.
1776 1899 *
1777 - * @since 1.9
1778 - * @access protected
1779 - * @author Grégory Viguier
1900 + * @since 1.9
1780 1901 *
1781 1902 * @param string $action The action performed behind this lock: 'optimizing' or 'restoring'.
1782 1903 * @return string The valid action.
1783 1904 */
@@ -1794,19 +1915,12 @@
1794 1915
1795 1916 return $action;
1796 1917 }
1797 1918
1798 -
1799 - /** ----------------------------------------------------------------------------------------- */
1800 - /** DATA ==================================================================================== */
1801 - /** ----------------------------------------------------------------------------------------- */
1802 -
1803 1919 /**
1804 1920 * Tell if a size already has optimization data.
1805 1921 *
1806 - * @since 1.9
1807 - * @access public
1808 - * @author Grégory Viguier
1922 + * @since 1.9
1809 1923 *
1810 1924 * @param string $size The size name.
1811 1925 * @return bool
1812 1926 */
@@ -1818,15 +1932,15 @@
1818 1932
1819 1933 /**
1820 1934 * Update the optimization data for a size.
1821 1935 *
1822 - * @since 1.9
1823 - * @access public
1824 - * @author Grégory Viguier
1936 + * @since 1.9
1937 + * @since 2.2 - Addition of the format in the call.
1825 1938 *
1826 - * @param object $response The API response.
1827 - * @param string $size The size name.
1828 - * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra).
1939 + * @param object $response The API response.
1940 + * @param string $size The size name.
1941 + * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra).
1942 + *
1829 1943 * @return array {
1830 1944 * The optimization data.
1831 1945 *
1832 1946 * @type int $level The optimization level.
@@ -1862,13 +1976,16 @@
1862 1976 } else {
1863 1977 /**
1864 1978 * Success.
1865 1979 */
1866 - $response = (object) array_merge( [
1867 - 'original_size' => 0,
1868 - 'new_size' => 0,
1869 - 'percent' => 0,
1870 - ], (array) $response );
1980 + $response = (object) array_merge(
1981 + [
1982 + 'original_size' => 0,
1983 + 'new_size' => 0,
1984 + 'percent' => 0,
1985 + ],
1986 + (array) $response
1987 + );
1871 1988
1872 1989 // Status.
1873 1990 $data['status'] = 'success';
1874 1991 $data['error'] = null;
@@ -1873,9 +1990,14 @@
1873 1990 $data['status'] = 'success';
1874 1991 $data['error'] = null;
1875 1992
1876 1993 // Size data.
1877 - $data['success'] = true;
1994 + $data['success'] = true;
1995 +
1996 + if ( property_exists( $response, 'message' ) ) {
1997 + $data['message'] = imagify_translate_api_message( $response->message );
1998 + }
1999 +
1878 2000 $data['original_size'] = $response->original_size;
1879 2001 $data['optimized_size'] = $response->new_size;
1880 2002 }
1881 2003
@@ -1883,10 +2005,9 @@
1883 2005
1884 2006 /**
1885 2007 * Filter the optimization data.
1886 2008 *
1887 - * @since 1.9
1888 - * @author Grégory Viguier
2009 + * @since 1.9
1889 2010 *
1890 2011 * @param array $data {
1891 2012 * The optimization data.
1892 2013 *
@@ -1903,8 +2024,24 @@
1903 2024 * @param object $media_data The DataInterface instance of the media.
1904 2025 */
1905 2026 $data = (array) apply_filters( "imagify{$_unauthorized}_file_optimization_data", $data, $response, $size, $level, $this->get_data() );
1906 2027
2028 + if ( property_exists( $response, 'message' ) && $this->is_next_gen_size( $size ) ) {
2029 + /**
2030 + * The API answered with a 200 and a message instead of a converted file: it declined the
2031 + * conversion, because the next-gen version would be heavier than the original or the file is
2032 + * already compressed. No next-gen file is written to disk in that case (see
2033 + * Imagify\Optimization\File::optimize()), so recording a success here would make the plugin
2034 + * report a next-gen version that does not exist.
2035 + *
2036 + * Store a terminal entry instead, flagged as a permanent refusal so the bulk queries can skip
2037 + * the media without re-sending it to the API on every run. Transient failures go through the
2038 + * WP_Error branch above and are not flagged, so they keep being retried.
2039 + */
2040 + $data['success'] = false;
2041 + $data['error'] = $data['message'];
2042 + $data['permanent_error'] = true;
2043 + }
1907 2044 // Store.
1908 2045 $this->get_data()->update_size_optimization_data( $size, $data );
1909 2046
1910 2047 return $data;
@@ -1909,21 +2046,34 @@
1909 2046
1910 2047 return $data;
1911 2048 }
1912 2049
2050 + /**
2051 + * Tell if a size name refers to a next-gen (AVIF or WebP) version.
2052 + *
2053 + * @param string $size The size name.
2054 + *
2055 + * @return bool
2056 + */
2057 + private function is_next_gen_size( $size ) {
2058 + $size = (string) $size;
1913 2059
1914 - /** ----------------------------------------------------------------------------------------- */
1915 - /** VARIOUS TOOLS =========================================================================== */
1916 - /** ----------------------------------------------------------------------------------------- */
2060 + foreach ( [ static::AVIF_SUFFIX, static::WEBP_SUFFIX ] as $suffix ) {
2061 + if ( '' !== $suffix && substr( $size, - strlen( $suffix ) ) === $suffix ) {
2062 + return true;
2063 + }
2064 + }
1917 2065
2066 + return false;
2067 + }
2068 +
1918 2069 /**
1919 2070 * Get a plugin’s option.
1920 2071 *
1921 - * @since 1.9
1922 - * @access protected
1923 - * @author Grégory Viguier
2072 + * @since 1.9
1924 2073 *
1925 - * @param string $option_name The option nme.
2074 + * @param string $option_name The option name.
2075 + *
1926 2076 * @return mixed
1927 2077 */
1928 2078 protected function get_option( $option_name ) {
1929 2079 if ( isset( $this->options[ $option_name ] ) ) {
@@ -1938,19 +2088,50 @@
1938 2088 /**
1939 2089 * Sanitize and validate an optimization level.
1940 2090 * If not provided (false, null), fallback to the level set in the plugin's settings.
1941 2091 *
1942 - * @since 1.9
1943 - * @access protected
1944 - * @author Grégory Viguier
2092 + * @since 1.9
1945 2093 *
1946 - * @param mixed $optimization_level The optimization level.
2094 + * @param mixed $optimization_level The optimization level.
2095 + *
1947 2096 * @return int
1948 2097 */
1949 2098 protected function sanitize_optimization_level( $optimization_level ) {
1950 2099 if ( ! is_numeric( $optimization_level ) ) {
2100 + if ( $this->get_option( 'lossless' ) ) {
2101 + return 0;
2102 + }
2103 +
1951 2104 return $this->get_option( 'optimization_level' );
1952 2105 }
1953 2106
1954 2107 return \Imagify_Options::get_instance()->sanitize_and_validate( 'optimization_level', $optimization_level );
2108 + }
2109 +
2110 + /**
2111 + * Tell if the media has AVIF versions.
2112 + *
2113 + * @since 2.2
2114 + *
2115 + * @return bool
2116 + */
2117 + public function has_avif() {
2118 + if ( ! $this->is_valid() ) {
2119 + return false;
2120 + }
2121 +
2122 + if ( ! $this->get_media()->is_image() ) {
2123 + return false;
2124 + }
2125 +
2126 + $data = $this->get_data()->get_optimization_data();
2127 +
2128 + if ( empty( $data['sizes'] ) ) {
2129 + return false;
2130 + }
2131 +
2132 + $needle = static::AVIF_SUFFIX . '";a:4:{s:7:"success";b:1;';
2133 + $data = maybe_serialize( $data['sizes'] );
2134 +
2135 + return is_string( $data ) && strpos( $data, $needle );
1955 2136 }
1956 2137 }