PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.2
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.2
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
imagify / classes / Optimization / File.php

File.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 2.3.2, at classes/Optimization/File.php

1,086 lines 28.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Imagify\Optimization;
3
4 use Imagify_Requirements;
5 use WP_Error;
6
7 /**
8 * A generic optimization class focused on the file itself.
9 *
10 * @since 1.9
11 * @author Grégory Viguier
12 */
13 class File {
14
15 /**
16 * Absolute path to the file.
17 *
18 * @var string
19 * @since 1.9
20 * @author Grégory Viguier
21 */
22 protected $path;
23
24 /**
25 * Tell if the file is an image.
26 *
27 * @var bool
28 * @since 1.9
29 * @see $this->is_image()
30 * @author Grégory Viguier
31 */
32 protected $is_image;
33
34 /**
35 * Store the file mime type + file extension (if the file is supported).
36 *
37 * @var array
38 * @since 1.9
39 * @see $this->get_file_type()
40 * @author Grégory Viguier
41 */
42 protected $file_type;
43
44 /**
45 * Filesystem object.
46 *
47 * @var \Imagify_Filesystem
48 * @since 1.9
49 * @author Grégory Viguier
50 */
51 protected $filesystem;
52
53 /**
54 * The editor instance used to resize the file.
55 *
56 * @var \WP_Image_Editor_Imagick|\WP_Image_Editor_GD|WP_Error.
57 * @since 1.9
58 * @author Grégory Viguier
59 */
60 protected $editor;
61
62 /**
63 * Used to cache the plugin’s options.
64 *
65 * @var array
66 * @since 1.9
67 * @author Grégory Viguier
68 */
69 protected $options = [];
70
71 /**
72 * The constructor.
73 *
74 * @since 1.9
75 * @author Grégory Viguier
76 *
77 * @param string $file_path Absolute path to the file.
78 */
79 public function __construct( $file_path ) {
80 $this->path = $file_path;
81 $this->filesystem = \Imagify_Filesystem::get_instance();
82 }
83
84 /**
85 * Tell if the file is valid.
86 *
87 * @since 1.9
88 * @author Grégory Viguier
89 *
90 * @return bool
91 */
92 public function is_valid() {
93 return (bool) $this->path;
94 }
95
96 /**
97 * Tell if the file can be processed.
98 *
99 * @since 1.9
100 * @author Grégory Viguier
101 *
102 * @return bool|WP_Error
103 */
104 public function can_be_processed() {
105 if ( ! $this->path ) {
106 return new \WP_Error( 'empty_path', __( 'File path is empty.', 'imagify' ) );
107 }
108
109 if ( ! empty( $this->filesystem->errors->errors ) ) {
110 return new \WP_Error( 'filesystem_error', __( 'Filesystem error.', 'imagify' ), $this->filesystem->errors );
111 }
112
113 if ( ! $this->filesystem->exists( $this->path ) ) {
114 return new \WP_Error(
115 'not_exists',
116 sprintf(
117 /* translators: %s is a file path. */
118 __( 'The file %s does not seem to exist.', 'imagify' ),
119 '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>'
120 )
121 );
122 }
123
124 if ( ! $this->filesystem->is_file( $this->path ) ) {
125 return new \WP_Error(
126 'not_a_file',
127 sprintf(
128 /* translators: %s is a file path. */
129 __( 'This does not seem to be a file: %s.', 'imagify' ),
130 '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>'
131 )
132 );
133 }
134
135 if ( ! $this->filesystem->is_writable( $this->path ) ) {
136 return new \WP_Error(
137 'not_writable',
138 sprintf(
139 /* translators: %s is a file path. */
140 __( 'The file %s does not seem to be writable.', 'imagify' ),
141 '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>'
142 )
143 );
144 }
145
146 $parent_folder = $this->filesystem->dir_path( $this->path );
147
148 if ( ! $this->filesystem->is_writable( $parent_folder ) ) {
149 return new \WP_Error(
150 'folder_not_writable',
151 sprintf(
152 /* translators: %s is a file path. */
153 __( 'The folder %s does not seem to be writable.', 'imagify' ),
154 '<code>' . esc_html( $this->filesystem->make_path_relative( $parent_folder ) ) . '</code>'
155 )
156 );
157 }
158
159 return true;
160 }
161
162
163 /** ----------------------------------------------------------------------------------------- */
164 /** EDITION ================================================================================= */
165 /** ----------------------------------------------------------------------------------------- */
166
167 /**
168 * Resize (and rotate) an image if it is bigger than the maximum width provided.
169 *
170 * @since 1.9
171 * @author Grégory Viguier
172 * @author Remy Perona
173 *
174 * @param array $dimensions {
175 * Array of image dimensions.
176 *
177 * @type int $width The image width.
178 * @type int $height The image height.
179 * }
180 * @param int $max_width Maximum width to resize to.
181 * @return string|WP_Error Path the the resized image. A WP_Error object on failure.
182 */
183 public function resize( $dimensions = [], $max_width = 0 ) {
184 $can_be_processed = $this->can_be_processed();
185
186 if ( is_wp_error( $can_be_processed ) ) {
187 return $can_be_processed;
188 }
189
190 if ( ! $max_width ) {
191 return new \WP_Error(
192 'no_resizing_threshold',
193 __( 'No threshold provided for resizing.', 'imagify' )
194 );
195 }
196
197 if ( ! $this->is_image() ) {
198 return new \WP_Error(
199 'not_an_image',
200 sprintf(
201 /* translators: %s is a file path. */
202 __( 'The file %s does not seem to be an image, and cannot be resized.', 'imagify' ),
203 '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>'
204 )
205 );
206 }
207
208 $editor = $this->get_editor();
209
210 if ( is_wp_error( $editor ) ) {
211 return $editor;
212 }
213
214 // Try to correct the auto-rotation if the info is available.
215 if ( $this->filesystem->can_get_exif() && 'image/jpeg' === $this->get_mime_type() ) {
216 $exif = $this->filesystem->get_image_exif( $this->path );
217 $orientation = isset( $exif['Orientation'] ) ? (int) $exif['Orientation'] : 1;
218
219 $this->rotate_editor_by_exif_orientation( $editor, $orientation );
220 }
221
222 if ( ! $dimensions ) {
223 $dimensions = $this->get_dimensions();
224 }
225
226 // Prevent removal of the exif data when resizing (only works with Imagick).
227 add_filter( 'image_strip_meta', '__return_false', 789 );
228
229 // Resize.
230 $new_sizes = wp_constrain_dimensions( $dimensions['width'], $dimensions['height'], $max_width );
231 $resized = $editor->resize( $new_sizes[0], $new_sizes[1], false );
232
233 // Remove the filter when we're done to prevent any conflict.
234 remove_filter( 'image_strip_meta', '__return_false', 789 );
235
236 if ( is_wp_error( $resized ) ) {
237 return $resized;
238 }
239
240 $resized_image_path = $editor->generate_filename( 'imagifyresized' );
241 $resized_image_saved = $editor->save( $resized_image_path );
242
243 if ( is_wp_error( $resized_image_saved ) ) {
244 return $resized_image_saved;
245 }
246
247 return $resized_image_path;
248 }
249
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 /**
356 * Create a thumbnail.
357 * Warning: If the destination file already exists, it will be overwritten.
358 *
359 * @since 1.9
360 * @author Grégory Viguier
361 *
362 * @param array $destination {
363 * The thumbnail data.
364 *
365 * @type string $path Path to the destination file.
366 * @type int $width The image width.
367 * @type int $height The image height.
368 * @type bool $crop True to crop, false to resize.
369 * @type bool $adjust_filename True to adjust the file name like what `$editor->multi_resize()` returns, like WP default behavior (default). False to prevent it, and use the file name from $path instead.
370 * }
371 * @return bool|array|WP_Error {
372 * A WP_Error object on error. True if the file exists.
373 * An array of thumbnail data if the file has just been created:
374 *
375 * @type string $file File name.
376 * @type int $width The image width.
377 * @type int $height The image height.
378 * @type string $mime-type The mime type.
379 * }
380 */
381 public function create_thumbnail( $destination ) {
382 $can_be_processed = $this->can_be_processed();
383
384 if ( is_wp_error( $can_be_processed ) ) {
385 return $can_be_processed;
386 }
387
388 if ( ! $this->is_image() ) {
389 return new WP_Error(
390 'not_an_image',
391 sprintf(
392 /* translators: %s is a file path. */
393 __( 'The file %s does not seem to be an image, and cannot be resized.', 'imagify' ),
394 '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>'
395 )
396 );
397 }
398
399 $editor = $this->get_editor();
400
401 if ( is_wp_error( $editor ) ) {
402 return $editor;
403 }
404
405 // Create the file.
406 $result = $editor->multi_resize( [ $destination ] );
407
408 if ( ! $result ) {
409 return new WP_Error( 'image_resize_error', __( 'The thumbnail could not be created.', 'imagify' ) );
410 }
411
412 $result = reset( $result );
413
414 $filename = $result['file'];
415 $source_thumb_path = $this->filesystem->dir_path( $this->path ) . $filename;
416
417 if ( ! isset( $destination['adjust_filename'] ) || $destination['adjust_filename'] ) {
418 // The file name can change from what we expected (1px wider, etc), let's use the resulting data to move the file to the right place.
419 $destination_thumb_path = $this->filesystem->dir_path( $destination['path'] ) . $filename;
420 } else {
421 // Respect what is set in $path.
422 $destination_thumb_path = $destination['path'];
423 $result['file'] = $this->filesystem->file_name( $destination['path'] );
424 }
425
426 if ( $source_thumb_path === $destination_thumb_path ) {
427 return $result;
428 }
429
430 $moved = $this->filesystem->move( $source_thumb_path, $destination_thumb_path, true );
431
432 if ( ! $moved ) {
433 return new WP_Error( 'move_error', __( 'The file could not be moved to its final destination.', 'imagify' ) );
434 }
435
436 return $result;
437 }
438
439 /**
440 * Backup a file.
441 *
442 * @since 1.9
443 * @since 1.9.8 Added $backup_source argument.
444 * @author Grégory Viguier
445 *
446 * @param string $backup_path The backup path.
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.
448 * @return bool|WP_Error True on success. False if the backup option is disabled. A WP_Error object on failure.
449 */
450 public function backup( $backup_path = null, $backup_source = null ) {
451 $can_be_processed = $this->can_be_processed();
452
453 if ( is_wp_error( $can_be_processed ) ) {
454 return $can_be_processed;
455 }
456
457 // Make sure the backups directory has no errors.
458 if ( ! $backup_path ) {
459 return new \WP_Error( 'wp_upload_error', __( 'Error while retrieving the backups directory path.', 'imagify' ) );
460 }
461
462 // Create sub-directories.
463 $created = $this->filesystem->make_dir( $this->filesystem->dir_path( $backup_path ) );
464
465 if ( ! $created ) {
466 return new \WP_Error( 'backup_dir_not_writable', __( 'The backup directory is not writable.', 'imagify' ) );
467 }
468
469 $path = $backup_source && $this->filesystem->exists( $backup_source ) ? $backup_source : $this->path;
470
471 /**
472 * Allow to overwrite the backup file if it already exists.
473 *
474 * @since 1.6.9
475 * @author Grégory Viguier
476 *
477 * @param bool $overwrite Whether to overwrite the backup file.
478 * @param string $path The file path.
479 * @param string $backup_path The backup path.
480 */
481 $overwrite = apply_filters( 'imagify_backup_overwrite_backup', false, $path, $backup_path );
482
483 // Copy the file.
484 $this->filesystem->copy( $path, $backup_path, $overwrite, FS_CHMOD_FILE );
485
486 // Make sure the backup copy exists.
487 if ( ! $this->filesystem->exists( $backup_path ) ) {
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 );
496 }
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
518 return true;
519 }
520
521 /**
522 * Optimize a file with Imagify.
523 *
524 * @since 1.9
525 * @author Grégory Viguier
526 *
527 * @param array $args {
528 * Optional. An array of arguments.
529 *
530 * @type bool $backup False to prevent backup. True to follow the user's setting. A backup can't be forced.
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.
532 * @type int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal).
533 * @type string $convert Set to 'webp' to convert the image to WebP, 'avif' to convert image to AVIF.
534 * @type string $context The context.
535 * @type int $original_size The file size, sent to the API.
536 * }
537 * @return \sdtClass|\WP_Error Optimized image data. A \WP_Error object on error.
538 */
539 public function optimize( $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 );
552
553 $can_be_processed = $this->can_be_processed();
554
555 if ( is_wp_error( $can_be_processed ) ) {
556 return $can_be_processed;
557 }
558
559 // Check if external HTTP requests are blocked.
560 if ( Imagify_Requirements::is_imagify_blocked() ) {
561 return new \WP_Error( 'http_block_external', __( 'External HTTP requests are blocked.', 'imagify' ) );
562 }
563
564 /**
565 * Fires before a media file optimization.
566 *
567 * @since 1.9
568 * @author Grégory Viguier
569 *
570 * @param string $path Absolute path to the media file.
571 * @param array $args Arguments passed to the method.
572 */
573 do_action( 'imagify_before_optimize_file', $this->path, $args );
574
575 /**
576 * Fires before to optimize the Image with Imagify.
577 *
578 * @since 1.0
579 * @deprecated
580 *
581 * @param string $path Absolute path to the image file.
582 * @param bool $backup True if a backup will be make.
583 */
584 do_action_deprecated( 'before_do_imagify', [ $this->path, $args['backup'] ], '1.9', 'imagify_before_optimize_file' );
585
586 if ( $args['backup'] ) {
587 $backup_result = $this->backup( $args['backup_path'], $args['backup_source'] );
588
589 if ( is_wp_error( $backup_result ) ) {
590 // Stop the process if we can't backup the file.
591 return $backup_result;
592 }
593 }
594
595 // Send file for optimization and fetch the response.
596 $data = [
597 'normal' => 0 === $args['optimization_level'],
598 'aggressive' => 1 === $args['optimization_level'],
599 'ultra' => 2 === $args['optimization_level'],
600 'keep_exif' => true,
601 'original_size' => $args['original_size'],
602 'context' => $args['context'],
603 ];
604
605 if ( $args['convert'] ) {
606 $data['convert'] = $args['convert'];
607 $format = $args['convert'];
608 }
609
610 $response = upload_imagify_image(
611 [
612 'image' => $this->path,
613 'data' => wp_json_encode( $data ),
614 ]
615 );
616
617 if ( is_wp_error( $response ) ) {
618 return new \WP_Error( 'api_error', $response->get_error_message() );
619 }
620
621 if ( ! function_exists( 'download_url' ) ) {
622 require_once ABSPATH . 'wp-admin/includes/file.php';
623 }
624
625 $temp_file = download_url( $response->image );
626
627 if ( is_wp_error( $temp_file ) ) {
628 return new \WP_Error( 'temp_file_not_found', $temp_file->get_error_message() );
629 }
630
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 $this->path = $destination_path;
663 $this->file_type = null;
664 $this->editor = null;
665 } else {
666 $destination_path = $this->path;
667 }
668
669 $moved = $this->filesystem->move( $temp_file, $destination_path, true );
670
671 if ( ! $moved ) {
672 return new \WP_Error( 'move_error', __( 'The file could not be moved to its final destination.', 'imagify' ) );
673 }
674
675 /**
676 * Fires after to optimize the Image with Imagify.
677 *
678 * @since 1.0
679 * @deprecated
680 *
681 * @param string $path Absolute path to the image file.
682 * @param bool $backup True if a backup has been made.
683 */
684 do_action_deprecated( 'after_do_imagify', [ $this->path, $args['backup'] ], '1.9', 'imagify_before_optimize_file' );
685
686 /**
687 * Fires after a media file optimization.
688 *
689 * @since 1.9
690 * @author Grégory Viguier
691 *
692 * @param string $path Absolute path to the media file.
693 * @param array $args Arguments passed to the method.
694 */
695 do_action( 'imagify_after_optimize_file', $this->path, $args );
696
697 return $response;
698 }
699
700
701 /** ----------------------------------------------------------------------------------------- */
702 /** IMAGE EDITOR (GD/IMAGEMAGICK) =========================================================== */
703 /** ----------------------------------------------------------------------------------------- */
704
705 /**
706 * Get an image editor instance (WP_Image_Editor_Imagick, WP_Image_Editor_GD).
707 *
708 * @since 1.9
709 * @author Grégory Viguier
710 *
711 * @return WP_Image_Editor_Imagick|WP_Image_Editor_GD|WP_Error
712 */
713 protected function get_editor() {
714 if ( isset( $this->editor ) ) {
715 return $this->editor;
716 }
717
718 $this->editor = wp_get_image_editor(
719 $this->path,
720 [
721 'methods' => $this->get_editor_methods(),
722 ]
723 );
724
725 if ( ! is_wp_error( $this->editor ) ) {
726 return $this->editor;
727 }
728
729 $this->editor = new \WP_Error(
730 'image_editor',
731 sprintf(
732 /* translators: %1$s is an error message, %2$s is a "More info?" link. */
733 __( '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' ),
734 $this->editor->get_error_message(),
735 '<a href="' . esc_url( imagify_get_external_url( 'documentation-imagick-gd' ) ) . '" target="_blank">' . __( 'More info?', 'imagify' ) . '</a>'
736 )
737 );
738
739 return $this->editor;
740 }
741
742 /**
743 * Get the image editor methods we will use.
744 *
745 * @since 1.9
746 * @author Grégory Viguier
747 *
748 * @return array
749 */
750 protected function get_editor_methods() {
751 static $methods;
752
753 if ( isset( $methods ) ) {
754 return $methods;
755 }
756
757 $methods = [
758 'resize',
759 'multi_resize',
760 'generate_filename',
761 'save',
762 ];
763
764 if ( $this->filesystem->can_get_exif() ) {
765 $methods[] = 'rotate';
766 }
767
768 return $methods;
769 }
770
771
772 /** ----------------------------------------------------------------------------------------- */
773 /** VARIOUS TOOLS =========================================================================== */
774 /** ----------------------------------------------------------------------------------------- */
775
776 /**
777 * Check if a file exceeds the weight limit (> 5mo).
778 *
779 * @since 1.9
780 * @author Grégory Viguier
781 *
782 * @return bool
783 */
784 public function is_exceeded() {
785 if ( ! $this->is_valid() ) {
786 return false;
787 }
788
789 $size = $this->filesystem->size( $this->path );
790
791 return $size > IMAGIFY_MAX_BYTES;
792 }
793
794 /**
795 * Tell if the current file is supported for a given context.
796 *
797 * @since 1.9
798 * @see imagify_get_mime_types()
799 * @author Grégory Viguier
800 *
801 * @param array $allowed_mime_types A list of allowed mime types.
802 * @return bool
803 */
804 public function is_supported( $allowed_mime_types ) {
805 return in_array( $this->get_mime_type(), $allowed_mime_types, true );
806 }
807
808 /**
809 * Tell if the file is an image.
810 *
811 * @since 1.9
812 * @author Grégory Viguier
813 *
814 * @return bool
815 */
816 public function is_image() {
817 if ( isset( $this->is_image ) ) {
818 return $this->is_image;
819 }
820
821 $this->is_image = strpos( $this->get_mime_type(), 'image/' ) === 0;
822
823 return $this->is_image;
824 }
825
826 /**
827 * Tell if the file is a pdf.
828 *
829 * @since 1.9
830 * @author Grégory Viguier
831 *
832 * @return bool
833 */
834 public function is_pdf() {
835 return 'application/pdf' === $this->get_mime_type();
836 }
837
838 /**
839 * Get the file mime type.
840 *
841 * @since 1.9
842 * @author Grégory Viguier
843 *
844 * @return string
845 */
846 public function get_mime_type() {
847 return $this->get_file_type()->type;
848 }
849
850 /**
851 * Get the file extension.
852 *
853 * @since 1.9
854 *
855 * @return string|false
856 */
857 public function get_extension() {
858 return $this->get_file_type()->ext;
859 }
860
861 /**
862 * Get the file path.
863 *
864 * @since 1.9
865 * @author Grégory Viguier
866 *
867 * @return string
868 */
869 public function get_path() {
870 return $this->path;
871 }
872
873 /**
874 * Replace the file extension by WebP.
875 *
876 * @since 1.9
877 * @author Grégory Viguier
878 *
879 * @return string|bool The file path on success. False if not an image or on failure.
880 */
881 public function get_path_to_webp() {
882 if ( ! $this->is_image() ) {
883 return false;
884 }
885
886 if ( $this->is_webp() ) {
887 return false;
888 }
889
890 return imagify_path_to_webp( $this->path );
891 }
892
893 /**
894 * Replace the file extension by its next-gen format extension.
895 *
896 * @since 2.2
897 *
898 * @param string $format the format we are targeting.
899 * @return string|bool The file path on success. False if not an image or on failure.
900 */
901 public function get_path_to_nextgen( string $format ) {
902 if ( ! $this->is_image() ) {
903 return false;
904 }
905
906 if ( $this->is_webp() || $this->is_avif() ) {
907 return false;
908 }
909
910 return imagify_path_to_nextgen( $this->path, $format );
911 }
912
913 /**
914 * Tell if the file is a WebP image.
915 * Rejects "path/to/.webp" files.
916 *
917 * @since 1.9
918 * @author Grégory Viguier
919 *
920 * @return bool
921 */
922 public function is_webp() {
923 return preg_match( '@(?!^|/|\\\)\.webp$@i', $this->path );
924 }
925
926 /**
927 * Tell if the file is an AVIF image.
928 * Rejects "path/to/.avif" files.
929 *
930 * @since 2.2
931 *
932 * @return bool
933 */
934 public function is_avif() {
935 return preg_match( '@(?!^|/|\\\)\.avif$@i', $this->path );
936 }
937
938 /**
939 * Tell if a file's actual content matches the given next-gen format, by reading its
940 * magic bytes. The file's extension can't be trusted here, since it may be a temp
941 * file downloaded with an unpredictable name (see download_url()).
942 *
943 * @since 2.3.2
944 *
945 * @param string $file_path Absolute path to the file to check.
946 * @param string $format 'webp' or 'avif'.
947 * @return bool
948 */
949 protected function is_file_format( $file_path, $format ) {
950 $contents = $this->filesystem->get_contents( $file_path );
951
952 if ( ! is_string( $contents ) || strlen( $contents ) < 12 ) {
953 return false;
954 }
955
956 if ( 'webp' === $format ) {
957 // RIFF....WEBP.
958 return 'RIFF' === substr( $contents, 0, 4 ) && 'WEBP' === substr( $contents, 8, 4 );
959 }
960
961 if ( 'avif' === $format ) {
962 return $this->is_avif_ftyp_box( $contents );
963 }
964
965 return false;
966 }
967
968 /**
969 * Tell if the given content starts with an AVIF `ftyp` box, i.e. its major brand or one of
970 * its compatible brands is `avif`/`avis`. A file can legitimately declare a major brand of
971 * `mif1`/`msf1` (generic HEIF-family brands) while listing `avif` only among the compatible
972 * brands, so both must be checked.
973 *
974 * @since 2.3.2
975 *
976 * @param string $contents The file content (or at least its leading bytes).
977 * @return bool
978 */
979 private function is_avif_ftyp_box( $contents ) {
980 if ( 'ftyp' !== substr( $contents, 4, 4 ) ) {
981 return false;
982 }
983
984 $avif_brands = [ 'avif', 'avis' ];
985
986 if ( in_array( substr( $contents, 8, 4 ), $avif_brands, true ) ) {
987 // Major brand.
988 return true;
989 }
990
991 // Box size (big-endian uint32), bounding how far the compatible brands list extends.
992 $box_size = unpack( 'N', substr( $contents, 0, 4 ) )[1];
993 $box_size = min( $box_size, strlen( $contents ) );
994
995 // Compatible brands: 4-byte entries, starting right after the minor version, at offset 16.
996 for ( $offset = 16; $offset + 4 <= $box_size; $offset += 4 ) {
997 if ( in_array( substr( $contents, $offset, 4 ), $avif_brands, true ) ) {
998 return true;
999 }
1000 }
1001
1002 return false;
1003 }
1004
1005 /**
1006 * Get the file mime type + file extension.
1007 *
1008 * @since 1.9
1009 * @see wp_check_filetype()
1010 * @author Grégory Viguier
1011 *
1012 * @return object {
1013 * @type string $ext The file extension.
1014 * @type string $type The mime type.
1015 * }
1016 */
1017 protected function get_file_type() {
1018 if ( isset( $this->file_type ) ) {
1019 return $this->file_type;
1020 }
1021
1022 $this->file_type = (object) [
1023 'ext' => '',
1024 'type' => '',
1025 ];
1026
1027 if ( ! $this->is_valid() ) {
1028 return $this->file_type;
1029 }
1030
1031 $this->file_type = (object) wp_check_filetype( $this->path );
1032
1033 return $this->file_type;
1034 }
1035
1036 /**
1037 * If the media is an image, get its width and height.
1038 *
1039 * @since 1.9
1040 * @author Grégory Viguier
1041 *
1042 * @return array
1043 */
1044 public function get_dimensions() {
1045 if ( ! $this->is_image() ) {
1046 return [
1047 'width' => 0,
1048 'height' => 0,
1049 ];
1050 }
1051
1052 $values = $this->filesystem->get_image_size( $this->path );
1053
1054 if ( empty( $values ) ) {
1055 return [
1056 'width' => 0,
1057 'height' => 0,
1058 ];
1059 }
1060
1061 return [
1062 'width' => $values['width'],
1063 'height' => $values['height'],
1064 ];
1065 }
1066
1067 /**
1068 * Get a plugin’s option.
1069 *
1070 * @since 1.9
1071 * @author Grégory Viguier
1072 *
1073 * @param string $option_name The option nme.
1074 * @return mixed
1075 */
1076 protected function get_option( $option_name ) {
1077 if ( isset( $this->options[ $option_name ] ) ) {
1078 return $this->options[ $option_name ];
1079 }
1080
1081 $this->options[ $option_name ] = get_imagify_option( $option_name );
1082
1083 return $this->options[ $option_name ];
1084 }
1085 }
1086