| @@ -37,12 +37,24 @@ | ||
| 37 | 37 | ]; |
| 38 | 38 | |
| 39 | 39 | /** |
| 40 | 40 | * Get merged settings with defaults. |
| 41 | + * | |
| 42 | + * AVIF is coerced back to WebP when the runtime cannot support it. A value of | |
| 43 | + * 'avif' can already be persisted (saved on a capable server, then the site | |
| 44 | + * moved or downgraded), so the coercion has to happen on read as well as on | |
| 45 | + * save — otherwise stored settings would keep driving AVIF conversion on a | |
| 46 | + * runtime that mis-measures the result. See supports_avif(). | |
| 41 | 47 | */ |
| 42 | 48 | public static function get_settings(): array { |
| 43 | - $saved = get_option(self::OPTION_KEY, []); | |
| 44 | - return wp_parse_args($saved, self::$defaults); | |
| 49 | + $saved = get_option(self::OPTION_KEY, []); | |
| 50 | + $settings = wp_parse_args($saved, self::$defaults); | |
| 51 | + | |
| 52 | + if (($settings['conversion_format'] ?? '') === 'avif' && !self::supports_avif()) { | |
| 53 | + $settings['conversion_format'] = 'webp'; | |
| 54 | + } | |
| 55 | + | |
| 56 | + return $settings; | |
| 45 | 57 | } |
| 46 | 58 | |
| 47 | 59 | /** |
| 48 | 60 | * Get default settings. |
| @@ -64,9 +76,9 @@ | ||
| 64 | 76 | */ |
| 65 | 77 | public static function sanitize(array $input): array { |
| 66 | 78 | return [ |
| 67 | 79 | 'enable_conversion' => !empty($input['enable_conversion']), |
| 68 | - 'conversion_format' => in_array($input['conversion_format'] ?? '', ['webp', 'avif'], true) ? $input['conversion_format'] : 'webp', | |
| 80 | + 'conversion_format' => self::sanitize_conversion_format($input['conversion_format'] ?? ''), | |
| 69 | 81 | 'conversion_quality' => max(1, min(100, (int) ($input['conversion_quality'] ?? 82))), |
| 70 | 82 | 'conversion_strategy' => in_array($input['conversion_strategy'] ?? '', ['replace', 'alongside'], true) ? $input['conversion_strategy'] : 'alongside', |
| 71 | 83 | 'convert_existing_sizes' => !empty($input['convert_existing_sizes']), |
| 72 | 84 | 'max_image_dimensions' => max(0, min(10000, (int) ($input['max_image_dimensions'] ?? 2560))), |
| @@ -79,11 +91,50 @@ | ||
| 79 | 91 | ]; |
| 80 | 92 | } |
| 81 | 93 | |
| 82 | 94 | /** |
| 95 | + * Resolve the target conversion format, refusing AVIF the runtime can't measure. | |
| 96 | + * | |
| 97 | + * Anything unrecognised falls back to WebP, as does AVIF on a runtime or GD/ | |
| 98 | + * Imagick build that cannot support it. This keeps an unsupported value from | |
| 99 | + * ever reaching the database. | |
| 100 | + */ | |
| 101 | + private static function sanitize_conversion_format(string $format): string { | |
| 102 | + if ($format === 'avif' && self::supports_avif()) { | |
| 103 | + return 'avif'; | |
| 104 | + } | |
| 105 | + | |
| 106 | + return 'webp'; | |
| 107 | + } | |
| 108 | + | |
| 109 | + /** | |
| 110 | + * Minimum PHP version at which AVIF output can be measured correctly. | |
| 111 | + * | |
| 112 | + * getimagesize() only learned to read real AVIF dimensions in PHP 8.2. On | |
| 113 | + * 8.1 it returns 0x0 for a valid AVIF file (and a truthy [0,0] for a | |
| 114 | + * truncated header, where 8.2 returns false). Every dimension read in this | |
| 115 | + * module guards with `$info && $info[0] > 0 && $info[1] > 0`, so on 8.1 the | |
| 116 | + * guard fails and: | |
| 117 | + * - the converter cannot sync post-downscale dimensions onto the | |
| 118 | + * attachment, leaving stale oversized width/height in metadata, and | |
| 119 | + * - the dimension injector then emits those stale values into the | |
| 120 | + * rendered HTML, which is worse than emitting none because the browser | |
| 121 | + * trusts them (a Core Web Vitals / CLS regression). | |
| 122 | + * | |
| 123 | + * imageavif() exists from 8.1 onward, so capability detection alone would | |
| 124 | + * pass and let an 8.1 site select AVIF. WebP is unaffected and is the | |
| 125 | + * default, so 8.1 sites are held to WebP until the runtime can measure AVIF. | |
| 126 | + */ | |
| 127 | + const AVIF_MIN_PHP_VERSION_ID = 80200; | |
| 128 | + | |
| 129 | + /** | |
| 83 | 130 | * Check if the server supports AVIF conversion. |
| 84 | 131 | */ |
| 85 | 132 | public static function supports_avif(): bool { |
| 133 | + // Encoding is not enough — the result also has to be measurable. | |
| 134 | + if (PHP_VERSION_ID < self::AVIF_MIN_PHP_VERSION_ID) { | |
| 135 | + return false; | |
| 136 | + } | |
| 86 | 137 | if (extension_loaded('imagick')) { |
| 87 | 138 | try { |
| 88 | 139 | $formats = \Imagick::queryFormats('AVIF'); |
| 89 | 140 | return !empty($formats); |