| @@ -244,8 +244,12 @@ | ||
| 244 | 244 | /** |
| 245 | 245 | * Optimize a media files. |
| 246 | 246 | * |
| 247 | 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. | |
| 248 | 252 | * |
| 249 | 253 | * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). |
| 250 | 254 | * @param array $args An array of optionnal arguments. |
| 251 | 255 | * |
| @@ -290,8 +294,12 @@ | ||
| 290 | 294 | /** |
| 291 | 295 | * Re-optimize a media files with a different level. |
| 292 | 296 | * |
| 293 | 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. | |
| 294 | 302 | * |
| 295 | 303 | * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). |
| 296 | 304 | * @param array $args An array of optionnal arguments. |
| 297 | 305 | * |
| @@ -346,8 +354,10 @@ | ||
| 346 | 354 | * @param array $args { |
| 347 | 355 | * An array of optionnal arguments. |
| 348 | 356 | * |
| 349 | 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. | |
| 350 | 360 | * } |
| 351 | 361 | * |
| 352 | 362 | * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. |
| 353 | 363 | */ |
| @@ -448,22 +458,32 @@ | ||
| 448 | 458 | if ( $new_args && is_array( $new_args ) ) { |
| 449 | 459 | $args = array_merge( $new_args, $args ); |
| 450 | 460 | } |
| 451 | 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 | + | |
| 452 | 468 | /** |
| 453 | 469 | * Push the item to the queue, save the queue in the DB, empty the queue. |
| 454 | 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). |
| 455 | 471 | */ |
| 456 | - MediaOptimization::get_instance()->push_to_queue( | |
| 457 | - [ | |
| 458 | - 'id' => $media->get_id(), | |
| 459 | - 'sizes' => $sizes, | |
| 460 | - 'optimization_level' => $optimization_level, | |
| 461 | - 'process_class' => get_class( $this ), | |
| 462 | - 'data' => $args, | |
| 463 | - ] | |
| 464 | - )->save(); | |
| 472 | + $item = [ | |
| 473 | + 'id' => $media->get_id(), | |
| 474 | + 'sizes' => $sizes, | |
| 475 | + 'optimization_level' => $optimization_level, | |
| 476 | + 'process_class' => get_class( $this ), | |
| 477 | + 'data' => $args, | |
| 478 | + ]; | |
| 465 | 479 | |
| 480 | + if ( $is_priority ) { | |
| 481 | + $item['priority'] = true; | |
| 482 | + } | |
| 483 | + | |
| 484 | + MediaOptimization::get_instance()->push_to_queue( $item )->save(); | |
| 485 | + | |
| 466 | 486 | return true; |
| 467 | 487 | } |
| 468 | 488 | |
| 469 | 489 | /** |
| @@ -504,28 +524,38 @@ | ||
| 504 | 524 | ) |
| 505 | 525 | ); |
| 506 | 526 | } |
| 507 | 527 | |
| 508 | - if ( $this->get_data()->get_size_data( $size, 'success' ) ) { // Bail out. | |
| 509 | - // This size is already optimized with Imagify, and must not be optimized again. | |
| 510 | - if ( $next_gen ) { | |
| 511 | - return new WP_Error( | |
| 512 | - 'size_is_successfully_optimized', | |
| 513 | - sprintf( | |
| 514 | - /* translators: %s is a size name. */ | |
| 515 | - __( 'The Next-Gen format for the size %s already exists.', 'imagify' ), | |
| 516 | - '<code>' . esc_html( $thumb_size ) . '</code>' | |
| 517 | - ) | |
| 518 | - ); | |
| 519 | - } else { | |
| 520 | - return new WP_Error( | |
| 521 | - 'size_is_successfully_optimized', | |
| 522 | - sprintf( | |
| 523 | - /* translators: %s is a size name. */ | |
| 524 | - __( 'The size %s is already optimized by Imagify.', 'imagify' ), | |
| 525 | - '<code>' . esc_html( $thumb_size ) . '</code>' | |
| 526 | - ) | |
| 527 | - ); | |
| 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( | |
| 543 | + /* translators: %s is a size name. */ | |
| 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( | |
| 552 | + /* translators: %s is a size name. */ | |
| 553 | + __( 'The size %s is already optimized by Imagify.', 'imagify' ), | |
| 554 | + '<code>' . esc_html( $thumb_size ) . '</code>' | |
| 555 | + ) | |
| 556 | + ); | |
| 557 | + } | |
| 528 | 558 | } |
| 529 | 559 | } |
| 530 | 560 | |
| 531 | 561 | /** |
| @@ -1083,8 +1113,17 @@ | ||
| 1083 | 1113 | if ( ! $copied ) { |
| 1084 | 1114 | return false; |
| 1085 | 1115 | } |
| 1086 | 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 | + | |
| 1087 | 1126 | if ( 'full' === $size ) { |
| 1088 | 1127 | /** |
| 1089 | 1128 | * We create a copy of the backup to be able to create a next-gen version from it. |
| 1090 | 1129 | * That means the optimization process will resize the file if needed, so there is nothing more to do here. |
| @@ -1562,8 +1601,24 @@ | ||
| 1562 | 1601 | return false; |
| 1563 | 1602 | } |
| 1564 | 1603 | |
| 1565 | 1604 | /** |
| 1605 | + * Tell if the next-gen file for a size is missing from disk. | |
| 1606 | + * | |
| 1607 | + * @since 2.3.2 | |
| 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. | |
| 1611 | + * @return bool | |
| 1612 | + */ | |
| 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 | + /** | |
| 1566 | 1621 | * Get suffix from format. |
| 1567 | 1622 | * |
| 1568 | 1623 | * @param string $format Format extension of next-gen image. |
| 1569 | 1624 | * @return string |
| @@ -1969,15 +2024,47 @@ | ||
| 1969 | 2024 | * @param object $media_data The DataInterface instance of the media. |
| 1970 | 2025 | */ |
| 1971 | 2026 | $data = (array) apply_filters( "imagify{$_unauthorized}_file_optimization_data", $data, $response, $size, $level, $this->get_data() ); |
| 1972 | 2027 | |
| 1973 | - if ( property_exists( $response, 'message' ) ) { | |
| 1974 | - $size = str_replace( $this->format, '', $size ); | |
| 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; | |
| 1975 | 2043 | } |
| 1976 | 2044 | // Store. |
| 1977 | 2045 | $this->get_data()->update_size_optimization_data( $size, $data ); |
| 1978 | 2046 | |
| 1979 | 2047 | return $data; |
| 2048 | + } | |
| 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; | |
| 2059 | + | |
| 2060 | + foreach ( [ static::AVIF_SUFFIX, static::WEBP_SUFFIX ] as $suffix ) { | |
| 2061 | + if ( '' !== $suffix && substr( $size, - strlen( $suffix ) ) === $suffix ) { | |
| 2062 | + return true; | |
| 2063 | + } | |
| 2064 | + } | |
| 2065 | + | |
| 2066 | + return false; | |
| 1980 | 2067 | } |
| 1981 | 2068 | |
| 1982 | 2069 | /** |
| 1983 | 2070 | * Get a plugin’s option. |