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/File.php +335 -115 1.10 → trunk View file →
@@ -1,13 +1,12 @@
1 1 <?php
2 2 namespace Imagify\Optimization;
3 3
4 4 use Imagify_Requirements;
5 +use WP_Error;
5 6
6 -defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' );
7 -
8 7 /**
9 - * A generic optimization class focussed on the file itself.
8 + * A generic optimization class focused on the file itself.
10 9 *
11 10 * @since 1.9
12 11 * @author Grégory Viguier
13 12 */
@@ -17,9 +16,8 @@
17 16 * Absolute path to the file.
18 17 *
19 18 * @var string
20 19 * @since 1.9
21 - * @access protected
22 20 * @author Grégory Viguier
23 21 */
24 22 protected $path;
25 23
@@ -27,9 +25,8 @@
27 25 * Tell if the file is an image.
28 26 *
29 27 * @var bool
30 28 * @since 1.9
31 - * @access protected
32 29 * @see $this->is_image()
33 30 * @author Grégory Viguier
34 31 */
35 32 protected $is_image;
@@ -38,9 +35,8 @@
38 35 * Store the file mime type + file extension (if the file is supported).
39 36 *
40 37 * @var array
41 38 * @since 1.9
42 - * @access protected
43 39 * @see $this->get_file_type()
44 40 * @author Grégory Viguier
45 41 */
46 42 protected $file_type;
@@ -49,9 +45,8 @@
49 45 * Filesystem object.
50 46 *
51 47 * @var \Imagify_Filesystem
52 48 * @since 1.9
53 - * @access protected
54 49 * @author Grégory Viguier
55 50 */
56 51 protected $filesystem;
57 52
@@ -59,9 +54,8 @@
59 54 * The editor instance used to resize the file.
60 55 *
61 56 * @var \WP_Image_Editor_Imagick|\WP_Image_Editor_GD|WP_Error.
62 57 * @since 1.9
63 - * @access protected
64 58 * @author Grégory Viguier
65 59 */
66 60 protected $editor;
67 61
@@ -69,9 +63,8 @@
69 63 * Used to cache the plugin’s options.
70 64 *
71 65 * @var array
72 66 * @since 1.9
73 - * @access protected
74 67 * @author Grégory Viguier
75 68 */
76 69 protected $options = [];
77 70
@@ -78,9 +71,8 @@
78 71 /**
79 72 * The constructor.
80 73 *
81 74 * @since 1.9
82 - * @access public
83 75 * @author Grégory Viguier
84 76 *
85 77 * @param string $file_path Absolute path to the file.
86 78 */
@@ -92,9 +84,8 @@
92 84 /**
93 85 * Tell if the file is valid.
94 86 *
95 87 * @since 1.9
96 - * @access public
97 88 * @author Grégory Viguier
98 89 *
99 90 * @return bool
100 91 */
@@ -105,9 +96,8 @@
105 96 /**
106 97 * Tell if the file can be processed.
107 98 *
108 99 * @since 1.9
109 - * @access public
110 100 * @author Grégory Viguier
111 101 *
112 102 * @return bool|WP_Error
113 103 */
@@ -123,9 +113,9 @@
123 113 if ( ! $this->filesystem->exists( $this->path ) ) {
124 114 return new \WP_Error(
125 115 'not_exists',
126 116 sprintf(
127 - /* translators: %s is a file path. */
117 + /* translators: %s is a file path. */
128 118 __( 'The file %s does not seem to exist.', 'imagify' ),
129 119 '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>'
130 120 )
131 121 );
@@ -134,9 +124,9 @@
134 124 if ( ! $this->filesystem->is_file( $this->path ) ) {
135 125 return new \WP_Error(
136 126 'not_a_file',
137 127 sprintf(
138 - /* translators: %s is a file path. */
128 + /* translators: %s is a file path. */
139 129 __( 'This does not seem to be a file: %s.', 'imagify' ),
140 130 '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>'
141 131 )
142 132 );
@@ -145,9 +135,9 @@
145 135 if ( ! $this->filesystem->is_writable( $this->path ) ) {
146 136 return new \WP_Error(
147 137 'not_writable',
148 138 sprintf(
149 - /* translators: %s is a file path. */
139 + /* translators: %s is a file path. */
150 140 __( 'The file %s does not seem to be writable.', 'imagify' ),
151 141 '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>'
152 142 )
153 143 );
@@ -158,9 +148,9 @@
158 148 if ( ! $this->filesystem->is_writable( $parent_folder ) ) {
159 149 return new \WP_Error(
160 150 'folder_not_writable',
161 151 sprintf(
162 - /* translators: %s is a file path. */
152 + /* translators: %s is a file path. */
163 153 __( 'The folder %s does not seem to be writable.', 'imagify' ),
164 154 '<code>' . esc_html( $this->filesystem->make_path_relative( $parent_folder ) ) . '</code>'
165 155 )
166 156 );
@@ -177,9 +167,8 @@
177 167 /**
178 168 * Resize (and rotate) an image if it is bigger than the maximum width provided.
179 169 *
180 170 * @since 1.9
181 - * @access public
182 171 * @author Grégory Viguier
183 172 * @author Remy Perona
184 173 *
185 174 * @param array $dimensions {
@@ -208,9 +197,9 @@
208 197 if ( ! $this->is_image() ) {
209 198 return new \WP_Error(
210 199 'not_an_image',
211 200 sprintf(
212 - /* translators: %s is a file path. */
201 + /* translators: %s is a file path. */
213 202 __( 'The file %s does not seem to be an image, and cannot be resized.', 'imagify' ),
214 203 '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>'
215 204 )
216 205 );
@@ -226,47 +215,9 @@
226 215 if ( $this->filesystem->can_get_exif() && 'image/jpeg' === $this->get_mime_type() ) {
227 216 $exif = $this->filesystem->get_image_exif( $this->path );
228 217 $orientation = isset( $exif['Orientation'] ) ? (int) $exif['Orientation'] : 1;
229 218
230 - switch ( $orientation ) {
231 - case 2:
232 - // Flip horizontally.
233 - $editor->flip( true, false );
234 - break;
235 - case 3:
236 - // Rotate 180 degrees or flip horizontally and vertically.
237 - // Flipping seems faster/uses less resources.
238 - $editor->flip( true, true );
239 - break;
240 - case 4:
241 - // Flip vertically.
242 - $editor->flip( false, true );
243 - break;
244 - case 5:
245 - // Rotate 90 degrees counter-clockwise and flip vertically.
246 - $result = $editor->rotate( 90 );
247 -
248 - if ( ! is_wp_error( $result ) ) {
249 - $editor->flip( false, true );
250 - }
251 - break;
252 - case 6:
253 - // Rotate 90 degrees clockwise (270 counter-clockwise).
254 - $editor->rotate( 270 );
255 - break;
256 - case 7:
257 - // Rotate 90 degrees counter-clockwise and flip horizontally.
258 - $result = $editor->rotate( 90 );
259 -
260 - if ( ! is_wp_error( $result ) ) {
261 - $editor->flip( true, false );
262 - }
263 - break;
264 - case 8:
265 - // Rotate 90 degrees counter-clockwise.
266 - $editor->rotate( 90 );
267 - break;
268 - }
219 + $this->rotate_editor_by_exif_orientation( $editor, $orientation );
269 220 }
270 221
271 222 if ( ! $dimensions ) {
272 223 $dimensions = $this->get_dimensions();
@@ -296,13 +247,117 @@
296 247 return $resized_image_path;
297 248 }
298 249
299 250 /**
251 + * Rotate/flip an image editor instance according to a JPEG EXIF "Orientation" value.
252 + *
253 + * @since 2.3.1
254 + *
255 + * @param \WP_Image_Editor_Imagick|\WP_Image_Editor_GD $editor The image editor instance.
256 + * @param int $orientation The EXIF "Orientation" value.
257 + * @return void
258 + */
259 + protected function rotate_editor_by_exif_orientation( $editor, $orientation ) {
260 + switch ( $orientation ) {
261 + case 2:
262 + // Flip horizontally.
263 + $editor->flip( true, false );
264 + break;
265 + case 3:
266 + // Rotate 180 degrees or flip horizontally and vertically.
267 + // Flipping seems faster/uses less resources.
268 + $editor->flip( true, true );
269 + break;
270 + case 4:
271 + // Flip vertically.
272 + $editor->flip( false, true );
273 + break;
274 + case 5:
275 + // Rotate 90 degrees counter-clockwise and flip vertically.
276 + $result = $editor->rotate( 90 );
277 +
278 + if ( ! is_wp_error( $result ) ) {
279 + $editor->flip( false, true );
280 + }
281 + break;
282 + case 6:
283 + // Rotate 90 degrees clockwise (270 counter-clockwise).
284 + $editor->rotate( 270 );
285 + break;
286 + case 7:
287 + // Rotate 90 degrees counter-clockwise and flip horizontally.
288 + $result = $editor->rotate( 90 );
289 +
290 + if ( ! is_wp_error( $result ) ) {
291 + $editor->flip( true, false );
292 + }
293 + break;
294 + case 8:
295 + // Rotate 90 degrees counter-clockwise.
296 + $editor->rotate( 90 );
297 + break;
298 + }
299 + }
300 +
301 + /**
302 + * Correct the EXIF orientation of the current file, in place, if needed.
303 + *
304 + * WordPress auto-rotates JPEGs with a non-default EXIF orientation on upload (the resulting,
305 + * rotated file has its orientation reset to 1), but Imagify's backup keeps a copy of the
306 + * original, un-rotated file. When a temporary working copy is created from that backup (for
307 + * example to generate a Next-Gen version of an already-optimized "full" size), the copy must
308 + * be rotated the same way WordPress would have rotated it, otherwise the resulting file (WebP,
309 + * AVIF...) ends up with the wrong orientation.
310 + *
311 + * This method must only ever be called on a disposable working copy: it saves the rotated
312 + * image over $this->path, and must never be used on the actual backup file.
313 + *
314 + * @since 2.3.1
315 + *
316 + * @return bool|WP_Error True if the file was rotated and saved. False if no rotation was
317 + * needed (or EXIF data isn't available/readable). A WP_Error object on
318 + * failure.
319 + */
320 + public function maybe_correct_exif_orientation() {
321 + if ( ! $this->filesystem->can_get_exif() || 'image/jpeg' !== $this->get_mime_type() ) {
322 + return false;
323 + }
324 +
325 + $exif = $this->filesystem->get_image_exif( $this->path );
326 + $orientation = isset( $exif['Orientation'] ) ? (int) $exif['Orientation'] : 1;
327 +
328 + if ( 1 === $orientation ) {
329 + // Nothing to correct: either there is no orientation data, or the file is already
330 + // upright (this also prevents rotating the same file twice).
331 + return false;
332 + }
333 +
334 + $editor = $this->get_editor();
335 +
336 + if ( is_wp_error( $editor ) ) {
337 + return $editor;
338 + }
339 +
340 + $this->rotate_editor_by_exif_orientation( $editor, $orientation );
341 +
342 + $saved = $editor->save( $this->path );
343 +
344 + if ( is_wp_error( $saved ) ) {
345 + return $saved;
346 + }
347 +
348 + // The file on disk changed: reset the cached data related to it.
349 + $this->file_type = null;
350 + $this->editor = null;
351 +
352 + return true;
353 + }
354 +
355 + /**
300 356 * Create a thumbnail.
301 357 * Warning: If the destination file already exists, it will be overwritten.
302 358 *
303 359 * @since 1.9
304 - * @access protected
305 360 * @author Grégory Viguier
306 361 *
307 362 * @param array $destination {
308 363 * The thumbnail data.
@@ -330,12 +385,12 @@
330 385 return $can_be_processed;
331 386 }
332 387
333 388 if ( ! $this->is_image() ) {
334 - return new \WP_Error(
389 + return new WP_Error(
335 390 'not_an_image',
336 391 sprintf(
337 - /* translators: %s is a file path. */
392 + /* translators: %s is a file path. */
338 393 __( 'The file %s does not seem to be an image, and cannot be resized.', 'imagify' ),
339 394 '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>'
340 395 )
341 396 );
@@ -350,9 +405,9 @@
350 405 // Create the file.
351 406 $result = $editor->multi_resize( [ $destination ] );
352 407
353 408 if ( ! $result ) {
354 - return new \WP_Error( 'image_resize_error', __( 'The thumbnail could not be created.', 'imagify' ) );
409 + return new WP_Error( 'image_resize_error', __( 'The thumbnail could not be created.', 'imagify' ) );
355 410 }
356 411
357 412 $result = reset( $result );
358 413
@@ -374,9 +429,9 @@
374 429
375 430 $moved = $this->filesystem->move( $source_thumb_path, $destination_thumb_path, true );
376 431
377 432 if ( ! $moved ) {
378 - return new \WP_Error( 'move_error', __( 'The file could not be moved to its final destination.', 'imagify' ) );
433 + return new WP_Error( 'move_error', __( 'The file could not be moved to its final destination.', 'imagify' ) );
379 434 }
380 435
381 436 return $result;
382 437 }
@@ -385,9 +440,8 @@
385 440 * Backup a file.
386 441 *
387 442 * @since 1.9
388 443 * @since 1.9.8 Added $backup_source argument.
389 - * @access public
390 444 * @author Grégory Viguier
391 445 *
392 446 * @param string $backup_path The backup path.
393 447 * @param string $backup_source Path to the file to backup. This is useful in WP 5.3+ when we want to optimize the full size: in that case we need to backup the original file.
@@ -430,14 +484,38 @@
430 484 $this->filesystem->copy( $path, $backup_path, $overwrite, FS_CHMOD_FILE );
431 485
432 486 // Make sure the backup copy exists.
433 487 if ( ! $this->filesystem->exists( $backup_path ) ) {
434 - return new \WP_Error( 'backup_doesnt_exist', __( 'The file could not be saved.', 'imagify' ), array(
435 - 'file_path' => $this->filesystem->make_path_relative( $path ),
436 - 'backup_path' => $this->filesystem->make_path_relative( $backup_path ),
437 - ) );
488 + return new \WP_Error(
489 + 'backup_doesnt_exist',
490 + __( 'The file could not be saved.', 'imagify' ),
491 + [
492 + 'file_path' => $this->filesystem->make_path_relative( $path ),
493 + 'backup_path' => $this->filesystem->make_path_relative( $backup_path ),
494 + ]
495 + );
438 496 }
439 497
498 + // Check if a '-scaled' version of the image exists.
499 + $scaled_path = preg_replace( '/(\.)([^\.]+)$/', '-scaled.$2', $backup_source );
500 + if ( $this->filesystem->exists( $scaled_path ) ) {
501 + // Create a backup path for the scaled image.
502 + $scaled_backup_path = preg_replace( '/(\.)([^\.]+)$/', '-scaled.$2', $backup_path );
503 + // Copy the '-scaled' version to the backup.
504 + $this->filesystem->copy( $scaled_path, $scaled_backup_path, $overwrite, FS_CHMOD_FILE );
505 +
506 + if ( ! $this->filesystem->exists( $scaled_backup_path ) ) {
507 + return new \WP_Error(
508 + 'backup_doesnt_exist',
509 + __( 'The file could not be saved.', 'imagify' ),
510 + [
511 + 'file_path' => $this->filesystem->make_path_relative( $scaled_path ),
512 + 'backup_path' => $this->filesystem->make_path_relative( $scaled_backup_path ),
513 + ]
514 + );
515 + }
516 + }
517 +
440 518 return true;
441 519 }
442 520
443 521 /**
@@ -443,9 +521,8 @@
443 521 /**
444 522 * Optimize a file with Imagify.
445 523 *
446 524 * @since 1.9
447 - * @access public
448 525 * @author Grégory Viguier
449 526 *
450 527 * @param array $args {
451 528 * Optional. An array of arguments.
@@ -452,10 +529,9 @@
452 529 *
453 530 * @type bool $backup False to prevent backup. True to follow the user's setting. A backup can't be forced.
454 531 * @type string $backup_path If a backup must be done, this is the path to use. Default is the backup path used for the WP Media Library.
455 532 * @type int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal).
456 - * @type bool $keep_exif To keep exif data or not.
457 - * @type string $convert Set to 'webp' to convert the image to WebP.
533 + * @type string $convert Set to 'webp' to convert the image to WebP, 'avif' to convert image to AVIF.
458 534 * @type string $context The context.
459 535 * @type int $original_size The file size, sent to the API.
460 536 * }
461 537 * @return \sdtClass|\WP_Error Optimized image data. A \WP_Error object on error.
@@ -460,18 +536,20 @@
460 536 * }
461 537 * @return \sdtClass|\WP_Error Optimized image data. A \WP_Error object on error.
462 538 */
463 539 public function optimize( $args = [] ) {
464 - $args = array_merge( [
465 - 'backup' => true,
466 - 'backup_path' => null,
467 - 'backup_source' => null,
468 - 'optimization_level' => 0,
469 - 'keep_exif' => true,
470 - 'convert' => '',
471 - 'context' => 'wp',
472 - 'original_size' => 0,
473 - ], $args );
540 + $args = array_merge(
541 + [
542 + 'backup' => true,
543 + 'backup_path' => null,
544 + 'backup_source' => null,
545 + 'optimization_level' => 0,
546 + 'convert' => '',
547 + 'context' => 'wp',
548 + 'original_size' => 0,
549 + ],
550 + $args
551 + );
474 552
475 553 $can_be_processed = $this->can_be_processed();
476 554
477 555 if ( is_wp_error( $can_be_processed ) ) {
@@ -490,9 +568,9 @@
490 568 * @author Grégory Viguier
491 569 *
492 570 * @param string $path Absolute path to the media file.
493 571 * @param array $args Arguments passed to the method.
494 - */
572 + */
495 573 do_action( 'imagify_before_optimize_file', $this->path, $args );
496 574
497 575 /**
498 576 * Fires before to optimize the Image with Imagify.
@@ -501,9 +579,9 @@
501 579 * @deprecated
502 580 *
503 581 * @param string $path Absolute path to the image file.
504 582 * @param bool $backup True if a backup will be make.
505 - */
583 + */
506 584 do_action_deprecated( 'before_do_imagify', [ $this->path, $args['backup'] ], '1.9', 'imagify_before_optimize_file' );
507 585
508 586 if ( $args['backup'] ) {
509 587 $backup_result = $this->backup( $args['backup_path'], $args['backup_source'] );
@@ -518,9 +596,9 @@
518 596 $data = [
519 597 'normal' => 0 === $args['optimization_level'],
520 598 'aggressive' => 1 === $args['optimization_level'],
521 599 'ultra' => 2 === $args['optimization_level'],
522 - 'keep_exif' => $args['keep_exif'],
600 + 'keep_exif' => true,
523 601 'original_size' => $args['original_size'],
524 602 'context' => $args['context'],
525 603 ];
526 604
@@ -525,14 +603,17 @@
525 603 ];
526 604
527 605 if ( $args['convert'] ) {
528 606 $data['convert'] = $args['convert'];
607 + $format = $args['convert'];
529 608 }
530 609
531 - $response = upload_imagify_image( [
532 - 'image' => $this->path,
533 - 'data' => wp_json_encode( $data ),
534 - ] );
610 + $response = upload_imagify_image(
611 + [
612 + 'image' => $this->path,
613 + 'data' => wp_json_encode( $data ),
614 + ]
615 + );
535 616
536 617 if ( is_wp_error( $response ) ) {
537 618 return new \WP_Error( 'api_error', $response->get_error_message() );
538 619 }
@@ -546,13 +627,58 @@
546 627 if ( is_wp_error( $temp_file ) ) {
547 628 return new \WP_Error( 'temp_file_not_found', $temp_file->get_error_message() );
548 629 }
549 630
550 - if ( 'webp' === $args['convert'] ) {
551 - $destination_path = $this->get_path_to_webp();
552 - $this->path = $destination_path;
553 - $this->file_type = null;
554 - $this->editor = null;
631 + $formats = [
632 + 'webp',
633 + 'avif',
634 + ];
635 + $is_nextgen_request = in_array( $args['convert'], $formats, true );
636 +
637 + if ( property_exists( $response, 'message' ) && ! $is_nextgen_request ) {
638 + $args['convert'] = '';
639 + }
640 +
641 + if ( $is_nextgen_request && property_exists( $response, 'message' ) ) {
642 + /*
643 + * The API can return a `message` alongside the source's own bytes instead of a
644 + * converted file (e.g. "Webp is less performant than original" or "already
645 + * compressed"). In that case the downloaded file is NOT the requested next-gen
646 + * format. Writing it to the next-gen path would create a corrupt file, and
647 + * writing it to `$this->path` would overwrite the original thumbnail. Verify the
648 + * actual bytes before doing either.
649 + */
650 + if ( ! $this->is_file_format( $temp_file, $args['convert'] ) ) {
651 + $this->filesystem->delete( $temp_file );
652 +
653 + return new \WP_Error(
654 + 'no_next_gen_returned',
655 + $response->message
656 + );
657 + }
658 + }
659 +
660 + if ( $is_nextgen_request ) {
661 + $destination_path = $this->get_path_to_nextgen( $args['convert'] );
662 +
663 + /*
664 + * get_path_to_nextgen() returns false when the file is not an image, or when it
665 + * already is in a next-gen format. Passing that along would hand an empty path to
666 + * the filesystem, which throws an uncaught ValueError on PHP 8 and kills the
667 + * background process mid-batch, leaving its lock behind and stalling the queue.
668 + */
669 + if ( empty( $destination_path ) ) {
670 + $this->filesystem->delete( $temp_file );
671 +
672 + return new \WP_Error(
673 + 'no_nextgen_destination',
674 + __( 'Could not determine the destination path for the next-gen file.', 'imagify' )
675 + );
676 + }
677 +
678 + $this->path = $destination_path;
679 + $this->file_type = null;
680 + $this->editor = null;
555 681 } else {
556 682 $destination_path = $this->path;
557 683 }
558 684
@@ -569,9 +695,9 @@
569 695 * @deprecated
570 696 *
571 697 * @param string $path Absolute path to the image file.
572 698 * @param bool $backup True if a backup has been made.
573 - */
699 + */
574 700 do_action_deprecated( 'after_do_imagify', [ $this->path, $args['backup'] ], '1.9', 'imagify_before_optimize_file' );
575 701
576 702 /**
577 703 * Fires after a media file optimization.
@@ -580,9 +706,9 @@
580 706 * @author Grégory Viguier
581 707 *
582 708 * @param string $path Absolute path to the media file.
583 709 * @param array $args Arguments passed to the method.
584 - */
710 + */
585 711 do_action( 'imagify_after_optimize_file', $this->path, $args );
586 712
587 713 return $response;
588 714 }
@@ -595,9 +721,8 @@
595 721 /**
596 722 * Get an image editor instance (WP_Image_Editor_Imagick, WP_Image_Editor_GD).
597 723 *
598 724 * @since 1.9
599 - * @access protected
600 725 * @author Grégory Viguier
601 726 *
602 727 * @return WP_Image_Editor_Imagick|WP_Image_Editor_GD|WP_Error
603 728 */
@@ -605,11 +730,14 @@
605 730 if ( isset( $this->editor ) ) {
606 731 return $this->editor;
607 732 }
608 733
609 - $this->editor = wp_get_image_editor( $this->path, [
610 - 'methods' => $this->get_editor_methods(),
611 - ] );
734 + $this->editor = wp_get_image_editor(
735 + $this->path,
736 + [
737 + 'methods' => $this->get_editor_methods(),
738 + ]
739 + );
612 740
613 741 if ( ! is_wp_error( $this->editor ) ) {
614 742 return $this->editor;
615 743 }
@@ -616,9 +744,9 @@
616 744
617 745 $this->editor = new \WP_Error(
618 746 'image_editor',
619 747 sprintf(
620 - /* translators: %1$s is an error message, %2$s is a "More info?" link. */
748 + /* translators: %1$s is an error message, %2$s is a "More info?" link. */
621 749 __( 'No php extensions are available to edit images on the server. ImageMagick or GD is required. The internal error is: %1$s. %2$s', 'imagify' ),
622 750 $this->editor->get_error_message(),
623 751 '<a href="' . esc_url( imagify_get_external_url( 'documentation-imagick-gd' ) ) . '" target="_blank">' . __( 'More info?', 'imagify' ) . '</a>'
624 752 )
@@ -630,9 +758,8 @@
630 758 /**
631 759 * Get the image editor methods we will use.
632 760 *
633 761 * @since 1.9
634 - * @access protected
635 762 * @author Grégory Viguier
636 763 *
637 764 * @return array
638 765 */
@@ -665,9 +792,8 @@
665 792 /**
666 793 * Check if a file exceeds the weight limit (> 5mo).
667 794 *
668 795 * @since 1.9
669 - * @access public
670 796 * @author Grégory Viguier
671 797 *
672 798 * @return bool
673 799 */
@@ -684,9 +810,8 @@
684 810 /**
685 811 * Tell if the current file is supported for a given context.
686 812 *
687 813 * @since 1.9
688 - * @access public
689 814 * @see imagify_get_mime_types()
690 815 * @author Grégory Viguier
691 816 *
692 817 * @param array $allowed_mime_types A list of allowed mime types.
@@ -699,9 +824,8 @@
699 824 /**
700 825 * Tell if the file is an image.
701 826 *
702 827 * @since 1.9
703 - * @access public
704 828 * @author Grégory Viguier
705 829 *
706 830 * @return bool
707 831 */
@@ -718,9 +842,8 @@
718 842 /**
719 843 * Tell if the file is a pdf.
720 844 *
721 845 * @since 1.9
722 - * @access public
723 846 * @author Grégory Viguier
724 847 *
725 848 * @return bool
726 849 */
@@ -731,9 +854,8 @@
731 854 /**
732 855 * Get the file mime type.
733 856 *
734 857 * @since 1.9
735 - * @access public
736 858 * @author Grégory Viguier
737 859 *
738 860 * @return string
739 861 */
@@ -743,13 +865,11 @@
743 865
744 866 /**
745 867 * Get the file extension.
746 868 *
747 - * @since 1.9
748 - * @access public
749 - * @author Grégory Viguier
869 + * @since 1.9
750 870 *
751 - * @return string|null
871 + * @return string|false
752 872 */
753 873 public function get_extension() {
754 874 return $this->get_file_type()->ext;
755 875 }
@@ -757,9 +877,8 @@
757 877 /**
758 878 * Get the file path.
759 879 *
760 880 * @since 1.9
761 - * @access public
762 881 * @author Grégory Viguier
763 882 *
764 883 * @return string
765 884 */
@@ -770,9 +889,8 @@
770 889 /**
771 890 * Replace the file extension by WebP.
772 891 *
773 892 * @since 1.9
774 - * @access public
775 893 * @author Grégory Viguier
776 894 *
777 895 * @return string|bool The file path on success. False if not an image or on failure.
778 896 */
@@ -788,13 +906,32 @@
788 906 return imagify_path_to_webp( $this->path );
789 907 }
790 908
791 909 /**
910 + * Replace the file extension by its next-gen format extension.
911 + *
912 + * @since 2.2
913 + *
914 + * @param string $format the format we are targeting.
915 + * @return string|bool The file path on success. False if not an image or on failure.
916 + */
917 + public function get_path_to_nextgen( string $format ) {
918 + if ( ! $this->is_image() ) {
919 + return false;
920 + }
921 +
922 + if ( $this->is_webp() || $this->is_avif() ) {
923 + return false;
924 + }
925 +
926 + return imagify_path_to_nextgen( $this->path, $format );
927 + }
928 +
929 + /**
792 930 * Tell if the file is a WebP image.
793 931 * Rejects "path/to/.webp" files.
794 932 *
795 933 * @since 1.9
796 - * @access public
797 934 * @author Grégory Viguier
798 935 *
799 936 * @return bool
800 937 */
@@ -802,12 +939,90 @@
802 939 return preg_match( '@(?!^|/|\\\)\.webp$@i', $this->path );
803 940 }
804 941
805 942 /**
943 + * Tell if the file is an AVIF image.
944 + * Rejects "path/to/.avif" files.
945 + *
946 + * @since 2.2
947 + *
948 + * @return bool
949 + */
950 + public function is_avif() {
951 + return preg_match( '@(?!^|/|\\\)\.avif$@i', $this->path );
952 + }
953 +
954 + /**
955 + * Tell if a file's actual content matches the given next-gen format, by reading its
956 + * magic bytes. The file's extension can't be trusted here, since it may be a temp
957 + * file downloaded with an unpredictable name (see download_url()).
958 + *
959 + * @since 2.3.2
960 + *
961 + * @param string $file_path Absolute path to the file to check.
962 + * @param string $format 'webp' or 'avif'.
963 + * @return bool
964 + */
965 + protected function is_file_format( $file_path, $format ) {
966 + $contents = $this->filesystem->get_contents( $file_path );
967 +
968 + if ( ! is_string( $contents ) || strlen( $contents ) < 12 ) {
969 + return false;
970 + }
971 +
972 + if ( 'webp' === $format ) {
973 + // RIFF....WEBP.
974 + return 'RIFF' === substr( $contents, 0, 4 ) && 'WEBP' === substr( $contents, 8, 4 );
975 + }
976 +
977 + if ( 'avif' === $format ) {
978 + return $this->is_avif_ftyp_box( $contents );
979 + }
980 +
981 + return false;
982 + }
983 +
984 + /**
985 + * Tell if the given content starts with an AVIF `ftyp` box, i.e. its major brand or one of
986 + * its compatible brands is `avif`/`avis`. A file can legitimately declare a major brand of
987 + * `mif1`/`msf1` (generic HEIF-family brands) while listing `avif` only among the compatible
988 + * brands, so both must be checked.
989 + *
990 + * @since 2.3.2
991 + *
992 + * @param string $contents The file content (or at least its leading bytes).
993 + * @return bool
994 + */
995 + private function is_avif_ftyp_box( $contents ) {
996 + if ( 'ftyp' !== substr( $contents, 4, 4 ) ) {
997 + return false;
998 + }
999 +
1000 + $avif_brands = [ 'avif', 'avis' ];
1001 +
1002 + if ( in_array( substr( $contents, 8, 4 ), $avif_brands, true ) ) {
1003 + // Major brand.
1004 + return true;
1005 + }
1006 +
1007 + // Box size (big-endian uint32), bounding how far the compatible brands list extends.
1008 + $box_size = unpack( 'N', substr( $contents, 0, 4 ) )[1];
1009 + $box_size = min( $box_size, strlen( $contents ) );
1010 +
1011 + // Compatible brands: 4-byte entries, starting right after the minor version, at offset 16.
1012 + for ( $offset = 16; $offset + 4 <= $box_size; $offset += 4 ) {
1013 + if ( in_array( substr( $contents, $offset, 4 ), $avif_brands, true ) ) {
1014 + return true;
1015 + }
1016 + }
1017 +
1018 + return false;
1019 + }
1020 +
1021 + /**
806 1022 * Get the file mime type + file extension.
807 1023 *
808 1024 * @since 1.9
809 - * @access protected
810 1025 * @see wp_check_filetype()
811 1026 * @author Grégory Viguier
812 1027 *
813 1028 * @return object {
@@ -837,9 +1052,8 @@
837 1052 /**
838 1053 * If the media is an image, get its width and height.
839 1054 *
840 1055 * @since 1.9
841 - * @access public
842 1056 * @author Grégory Viguier
843 1057 *
844 1058 * @return array
845 1059 */
@@ -852,8 +1066,15 @@
852 1066 }
853 1067
854 1068 $values = $this->filesystem->get_image_size( $this->path );
855 1069
1070 + if ( empty( $values ) ) {
1071 + return [
1072 + 'width' => 0,
1073 + 'height' => 0,
1074 + ];
1075 + }
1076 +
856 1077 return [
857 1078 'width' => $values['width'],
858 1079 'height' => $values['height'],
859 1080 ];
@@ -862,9 +1083,8 @@
862 1083 /**
863 1084 * Get a plugin’s option.
864 1085 *
865 1086 * @since 1.9
866 - * @access protected
867 1087 * @author Grégory Viguier
868 1088 *
869 1089 * @param string $option_name The option nme.
870 1090 * @return mixed