PluginProbe
Image Optimizer – Compress Images and Convert to WebP or AVIF / 1.7.7
Image Optimizer – Compress Images and Convert to WebP or AVIF v1.7.7
1.7.7 1.7.6 1.7.5 1.7.4 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.2.1 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 All 33 releases
← All changes | modules/optimization/classes/optimize-image.php +180 -68 1.0.01.7.7 View file →
@@ -1,29 +1,44 @@
1 1 <?php
2 2
3 -namespace ImageOptimizer\Modules\Optimization\Classes;
3 +namespace ImageOptimization\Modules\Optimization\Classes;
4 4
5 -use ImageOptimizer\Classes\File_Utils;
6 -use ImageOptimizer\Classes\Image\{
5 +use ImageOptimization\Classes\File_System\{
6 + Exceptions\File_System_Operation_Error,
7 + File_System,
8 +};
9 +use ImageOptimization\Classes\File_Utils;
10 +use ImageOptimization\Classes\Image\{
11 + Exceptions\Image_Backup_Creation_Error,
7 12 Exceptions\Invalid_Image_Exception,
8 13 Image,
9 14 Image_Backup,
15 + Image_Conversion,
16 + Image_DB_Update,
17 + Image_Dimensions,
10 18 Image_Meta,
11 19 Image_Status,
12 20 WP_Image_Meta
13 21 };
14 -use ImageOptimizer\Classes\Utils;
15 -use ImageOptimizer\Modules\Oauth\Components\Connect;
16 -use ImageOptimizer\Modules\Optimization\Classes\Exceptions\Image_Optimization_Error;
17 -use ImageOptimizer\Modules\Settings\Classes\Settings;
22 +use ImageOptimization\Classes\Logger;
23 +use ImageOptimization\Classes\Utils;
24 +use ImageOptimization\Classes\Exceptions\Quota_Exceeded_Error;
25 +use ImageOptimization\Modules\Optimization\Classes\{
26 + Exceptions\Image_File_Already_Exists_Error,
27 + Exceptions\Image_Optimization_Error,
28 + Exceptions\Bulk_Token_Expired_Error,
29 + Exceptions\Image_Already_Optimized_Error,
30 +};
31 +use ImageOptimization\Modules\Connect\Classes\Exceptions\Connection_Error;
32 +use ImageOptimization\Modules\Settings\Classes\Settings;
33 +use Throwable;
18 34
35 +use ImageOptimization\Plugin;
36 +
19 37 if ( ! defined( 'ABSPATH' ) ) {
20 38 exit; // Exit if accessed directly.
21 39 }
22 40
23 -require_once ABSPATH . 'wp-admin/includes/file.php';
24 -WP_Filesystem();
25 -
26 41 /**
27 42 * The class is responsible for the optimization logic itself. It gets an image file, runs
28 43 * backup process if needed, sends a file to the service, stores the result and updates image meta.
29 44 *
@@ -29,9 +44,9 @@
29 44 *
30 45 * This class is used by manual, bulk and on-upload optimization flows.
31 46 */
32 47 class Optimize_Image {
33 - private const IMAGE_OPTIMIZE_ENDPOINT = 'image/optimize';
48 + public const IMAGE_OPTIMIZE_ENDPOINT = 'image/optimize';
34 49
35 50 protected ?Image $image;
36 51 protected WP_Image_Meta $wp_meta;
37 52 protected string $initiator;
@@ -37,17 +52,22 @@
37 52 protected string $initiator;
38 53 protected ?string $bulk_token;
39 54 private string $current_image_path;
40 55 private string $current_image_size;
41 - private bool $convert_to_webp;
56 + private bool $keep_backups;
57 + private array $current_size_duplicates;
58 + protected Image_Conversion $image_conversion;
59 + private bool $is_reoptimize;
42 60
43 61 /**
44 - * @throws Image_Optimization_Error
62 + * @throws Quota_Exceeded_Error|Connection_Error|Bulk_Token_Expired_Error|Image_File_Already_Exists_Error|Image_Optimization_Error
45 63 */
46 64 public function optimize(): void {
47 65 $sizes_enabled = Settings::get( Settings::CUSTOM_SIZES_OPTION_NAME );
48 66 $sizes_exist = $this->wp_meta->get_size_keys();
49 67
68 + Logger::debug( "Start optimization of {$this->image->get_id()}" );
69 +
50 70 foreach ( $sizes_exist as $size_exist ) {
51 71 // If some image sizes optimization is disabled in settings, we check if the current one is still enabled
52 72 if (
53 73 'all' !== $sizes_enabled &&
@@ -56,59 +76,82 @@
56 76 ) {
57 77 continue;
58 78 }
59 79
80 + // Elementor editor generates thumbnails we don't need to optimize.
81 + if ( str_starts_with( $size_exist, 'elementor_' ) ) {
82 + continue;
83 + }
84 +
60 85 $image_meta = new Image_Meta( $this->image->get_id() );
61 86
62 87 // If the current size was already optimized -- ignore it.
63 - if ( in_array( $size_exist, $image_meta->get_optimized_sizes(), true ) ) {
88 + if ( ! $this->is_reoptimize && in_array( $size_exist, $image_meta->get_optimized_sizes(), true ) ) {
89 + Logger::debug( "Size `$size_exist` is already optimized" );
90 +
64 91 continue;
65 92 }
66 93
67 94 if ( ! file_exists( $this->image->get_file_path( $size_exist ) ) ) {
68 - throw new Image_Optimization_Error( esc_html__( 'File is missing. Verify the upload', 'image-optimizer' ) );
95 + Logger::debug( "Can't access file for size `$size_exist`" );
96 +
97 + continue;
69 98 }
70 99
71 100 $this->current_image_size = $size_exist;
101 + $this->current_size_duplicates = $this->wp_meta->get_size_duplicates( $size_exist );
72 102 $this->current_image_path = $this->image->get_file_path( $size_exist );
73 103
74 104 $this->optimize_current_size();
75 105
76 106 $this->current_image_size = '';
107 + $this->current_size_duplicates = [];
77 108 $this->current_image_path = '';
78 109 }
79 110
80 111 $this->mark_as_optimized();
112 +
113 + if ( ! $this->keep_backups ) {
114 + Image_Backup::remove( $this->image->get_id() );
115 + }
116 +
117 + Logger::debug( "End optimization of {$this->image->get_id()}" );
81 118 }
82 119
83 - /**
84 - * @throws Image_Optimization_Error
85 - */
86 120 private function optimize_current_size(): void {
87 - $response = $this->send_file();
121 + try {
122 + $original_path = $this->current_image_path;
123 + $response = $this->send_file();
88 124
89 - if ( is_wp_error( $response ) ) {
90 - throw new Image_Optimization_Error( $response->get_error_message() );
91 - }
125 + $optimized_size = (int) $response->optimizedSize; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
126 + $optimized_image_binary = base64_decode( $response->image, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
92 127
93 - $original_size = (int) $response->originalSize; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
94 - $optimized_size = (int) $response->optimizedSize; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
95 - $optimized_image_binary = base64_decode( $response->image, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
96 -
97 - // Sometimes the BE returns images bigger than original ones. In that case, we don't want to replace the original.
98 - if ( $original_size > $optimized_size ) {
99 128 $this->replace_image_file( $optimized_image_binary );
100 129 $this->update_attachment_meta( $optimized_size );
101 - } else {
102 - $this->update_attachment_meta( $original_size );
130 +
131 + if ( $original_path !== $this->current_image_path ) {
132 + $this->update_posts( $original_path, $this->current_image_path );
133 + }
134 +
135 + // This should only be updated after meta
136 + $this->update_attachment_post();
137 + } catch ( Image_Already_Optimized_Error $iao ) {
138 + // If we can't optimize it further, just update the meta
139 + $original_size = $this->wp_meta->get_file_size( $this->current_image_size )
140 + ?? File_System::size( $this->image->get_file_path( $this->current_image_size ) );
141 +
142 + $this->update_attachment_meta( $original_size, true );
143 + $this->update_attachment_post( true );
144 + } catch ( Bulk_Token_Expired_Error | Quota_Exceeded_Error | Connection_Error | Image_File_Already_Exists_Error $e ) {
145 + throw $e;
146 + } catch ( Throwable $t ) {
147 + // In case of anything else
148 + throw new Image_Optimization_Error( esc_html( $t->getMessage() ) );
103 149 }
104 -
105 - // This should only be updated after meta
106 - $this->update_attachment_post();
107 150 }
108 151
109 152 private function send_file() {
110 - $connect_status = Connect::check_connect_status();
153 + $connect_status = Plugin::instance()->modules_manager->get_modules( 'connect-manager' )->connect_instance->get_connect_status();
111 154 $headers = [
112 155 'access_token' => $connect_status->access_token ?? '',
113 156 ];
114 157
@@ -117,9 +160,9 @@
117 160 }
118 161
119 162 $optimization_options = [
120 163 'compression_level' => Settings::get( Settings::COMPRESSION_LEVEL_OPTION_NAME ),
121 - 'convert_to_webp' => $this->convert_to_webp,
164 + 'convert_to_format' => $this->image_conversion->get_current_conversion_option(),
122 165 'strip_exif' => Settings::get( Settings::STRIP_EXIF_METADATA_OPTION_NAME ),
123 166 ];
124 167
125 168 if ( Settings::get( Settings::RESIZE_LARGER_IMAGES_OPTION_NAME ) ) {
@@ -125,14 +168,18 @@
125 168 if ( Settings::get( Settings::RESIZE_LARGER_IMAGES_OPTION_NAME ) ) {
126 169 $optimization_options['resize'] = Settings::get( Settings::RESIZE_LARGER_IMAGES_SIZE_OPTION_NAME );
127 170 }
128 171
129 - return Utils::get_api_client()->make_request(
172 + $image_key = wp_generate_password( 15, false );
173 +
174 + $response = Utils::get_api_client()->make_request(
130 175 'POST',
131 176 self::IMAGE_OPTIMIZE_ENDPOINT,
132 177 [
133 178 'initiator' => $this->initiator,
134 179 'image_url' => $this->image->get_url( $this->current_image_size ),
180 + 'image_key' => $image_key,
181 + 'checksum' => md5_file( $this->current_image_path ),
135 182 'attachment_id' => $this->image->get_id(),
136 183 'attachment_parent_id' => Image::SIZE_FULL === $this->current_image_size ? 0 : $this->image->get_id(),
137 184 'image_optimization_settings' => base64_encode( wp_json_encode( $optimization_options ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
138 185 ],
@@ -139,13 +186,34 @@
139 186 $headers,
140 187 $this->current_image_path,
141 188 'image'
142 189 );
190 +
191 + if ( isset( $response->stats ) ) {
192 + Plugin::instance()->modules_manager->get_modules( 'connect-manager' )->connect_instance->update_usage_data( $response->stats );
193 + }
194 +
195 + if ( ! isset( $response->imageKey ) || $image_key !== $response->imageKey ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
196 + Logger::error( "Image key must be $image_key, instead got $response->imageKey" );
197 +
198 + throw new Image_Optimization_Error( esc_html__( 'Service response is incorrect', 'image-optimization' ) );
199 + }
200 +
201 + $received_file_hash = md5( base64_decode( $response->image, true ) );
202 +
203 + if ( ! isset( $response->checksum ) || $received_file_hash !== $response->checksum ) {
204 + Logger::error( "Image key must be $response->checksum, instead calculated $received_file_hash" );
205 +
206 + throw new Image_Optimization_Error( esc_html__( 'Service response is incorrect', 'image-optimization' ) );
207 + }
208 +
209 + return $response;
143 210 }
144 211
212 + /**
213 + * @throws File_System_Operation_Error|Image_Backup_Creation_Error|Image_File_Already_Exists_Error
214 + */
145 215 private function replace_image_file( string $file_data ): void {
146 - global $wp_filesystem;
147 -
148 216 $path = $this->current_image_path;
149 217
150 218 // If we have backups disabled, we want to make sure we can download and save new file before we
151 219 // remove an existing one.
@@ -150,23 +218,40 @@
150 218 // If we have backups disabled, we want to make sure we can download and save new file before we
151 219 // remove an existing one.
152 220 $tmp_path = File_Utils::replace_extension( $path, 'tmp' );
153 221
154 - $wp_filesystem->put_contents( $tmp_path, $file_data );
222 + File_System::put_contents( $tmp_path, $file_data );
155 223
156 - // The original file doesn't exist after any of these operations.
157 - if ( Settings::get( Settings::BACKUP_ORIGINAL_IMAGES_OPTION_NAME ) ) {
224 + if ( $this->image_conversion->is_enabled() ) {
225 + $converted_path = File_Utils::replace_extension( $tmp_path, $this->image_conversion->get_current_file_extension() );
226 + $original_is_already_converted = strtolower( $path ) === strtolower( $converted_path );
227 +
228 + if ( ! $original_is_already_converted && File_System::exists( $converted_path ) ) {
229 + throw new Image_File_Already_Exists_Error();
230 + }
231 +
232 + // We want to keep both original as a fallback and optimized webp to prevent 404s
233 + if ( ! $original_is_already_converted ) {
234 + $this->keep_backups = true;
235 +
236 + ( new Image_Meta( $this->image->get_id() ) )
237 + ->set_image_backup_path( $this->current_image_size, $this->current_image_path )
238 + ->save();
239 + }
240 +
241 + if ( $original_is_already_converted ) {
242 + Image_Backup::create( $this->image->get_id(), $this->current_image_size, $this->current_image_path );
243 + }
244 +
245 + File_System::move( $tmp_path, $converted_path, true );
246 +
247 + $path = $converted_path;
248 + } else {
158 249 Image_Backup::create( $this->image->get_id(), $this->current_image_size, $this->current_image_path );
159 - } else {
160 - $wp_filesystem->delete( $path, false, 'f' );
161 - }
162 250
163 - if ( $this->convert_to_webp ) {
164 - $path = File_Utils::replace_extension( $tmp_path, 'webp' );
251 + File_System::move( $tmp_path, $path, true );
165 252 }
166 253
167 - $wp_filesystem->move( $tmp_path, $path, true );
168 -
169 254 if ( Image::SIZE_FULL === $this->current_image_size ) {
170 255 // Drop WP caches
171 256 update_attached_file( $this->image->get_id(), $path );
172 257 }
@@ -179,16 +264,20 @@
179 264 * Updates attachment records in the `wp_posts` table.
180 265 *
181 266 * @return void
182 267 */
183 - private function update_attachment_post() {
268 + private function update_attachment_post( ?bool $is_fully_optimized = false ) {
184 269 $update_query = [];
185 270
186 - if ( $this->convert_to_webp ) {
271 + if ( $this->image_conversion->is_enabled() && ! $is_fully_optimized ) {
187 272 $attachment_object = $this->image->get_attachment_object();
188 273
189 - $update_query['guid'] = File_Utils::replace_extension( $attachment_object->guid, 'webp' );
190 - $update_query['post_mime_type'] = 'image/webp';
274 + $update_query['guid'] = File_Utils::replace_extension(
275 + $attachment_object->guid,
276 + $this->image_conversion->get_current_file_extension()
277 + );
278 +
279 + $update_query['post_mime_type'] = $this->image_conversion->get_current_mime_type();
191 280 }
192 281
193 282 $update_query['post_modified'] = current_time( 'mysql' );
194 283 $update_query['post_modified_gmt'] = current_time( 'mysql', true );
@@ -199,30 +288,34 @@
199 288 /**
200 289 * Updates attachment records in the `wp_postmeta` table.
201 290 *
202 291 * @param int $optimized_size
292 + * @param bool|null $is_fully_optimized
203 293 *
204 294 * @return void
205 295 */
206 - private function update_attachment_meta( int $optimized_size ) {
296 + private function update_attachment_meta( int $optimized_size, ?bool $is_fully_optimized = false ) {
207 297 $meta = new Image_Meta( $this->image->get_id() );
298 + $dimensions = Image_Dimensions::get_by_path( $this->current_image_path );
208 299
209 - list($width, $height) = getimagesize( $this->current_image_path );
300 + $sizes_to_update = [ $this->current_image_size, ...$this->current_size_duplicates ];
210 301
211 - $meta
212 - ->set_compression_level( Settings::get( Settings::COMPRESSION_LEVEL_OPTION_NAME ) )
213 - ->add_optimized_size( $this->current_image_size )
214 - ->add_original_data( $this->current_image_size, $this->wp_meta->get_size_data( $this->current_image_size ) );
302 + foreach ( $sizes_to_update as $size ) {
303 + $meta
304 + ->set_compression_level( Settings::get( Settings::COMPRESSION_LEVEL_OPTION_NAME ) )
305 + ->add_optimized_size( $size )
306 + ->add_original_data( $size, $this->wp_meta->get_size_data( $size ) );
215 307
216 - $this->wp_meta
217 - ->set_width( $this->current_image_size, $width )
218 - ->set_height( $this->current_image_size, $height )
219 - ->set_file_size( $this->current_image_size, $optimized_size );
308 + $this->wp_meta
309 + ->set_width( $size, $dimensions->width )
310 + ->set_height( $size, $dimensions->height )
311 + ->set_file_size( $size, $optimized_size );
220 312
221 - if ( $this->convert_to_webp ) {
222 - $this->wp_meta
223 - ->set_file_path( $this->current_image_size, $this->current_image_path )
224 - ->set_mime_type( $this->current_image_size, 'image/webp' );
313 + if ( $this->image_conversion->is_enabled() && ! $is_fully_optimized ) {
314 + $this->wp_meta
315 + ->set_file_path( $size, $this->current_image_path )
316 + ->set_mime_type( $size, $this->image_conversion->get_current_mime_type() );
317 + }
225 318 }
226 319
227 320 $meta->save();
228 321 $this->wp_meta->save();
@@ -228,8 +321,24 @@
228 321 $this->wp_meta->save();
229 322 }
230 323
231 324 /**
325 + * If we change an image extension, we should walk through the wp_posts table and update all the
326 + * hardcoded image links to prevent 404s.
327 + *
328 + * @param string $old_path Previous image path
329 + * @param string $new_path Current image path
330 + *
331 + * @return void
332 + */
333 + private function update_posts( string $old_path, string $new_path ) {
334 + Image_DB_Update::update_posts_table_urls(
335 + File_Utils::get_url_from_path( $old_path ),
336 + File_Utils::get_url_from_path( $new_path )
337 + );
338 + }
339 +
340 + /**
232 341 * Changes image status after all image sizes were optimized.
233 342 *
234 343 * @return void
235 344 */
@@ -235,8 +344,9 @@
235 344 */
236 345 private function mark_as_optimized() {
237 346 ( new Image_Meta( $this->image->get_id() ) )
238 347 ->set_status( Image_Status::OPTIMIZED )
348 + ->set_error_type( null )
239 349 ->save();
240 350 }
241 351
242 352 /**
@@ -241,12 +351,14 @@
241 351
242 352 /**
243 353 * @throws Invalid_Image_Exception
244 354 */
245 - public function __construct( int $image_id, string $initiator, ?string $bulk_token = null ) {
355 + public function __construct( int $image_id, string $initiator, ?string $bulk_token = null, ?bool $is_reoptimize = false ) {
246 356 $this->image = new Image( $image_id );
247 357 $this->wp_meta = new WP_Image_Meta( $image_id, $this->image );
248 358 $this->initiator = $initiator;
249 359 $this->bulk_token = $bulk_token;
250 - $this->convert_to_webp = Settings::get( Settings::CONVERT_TO_WEBP_OPTION_NAME );
360 + $this->is_reoptimize = $is_reoptimize;
361 + $this->image_conversion = new Image_Conversion();
362 + $this->keep_backups = Settings::get( Settings::BACKUP_ORIGINAL_IMAGES_OPTION_NAME );
251 363 }
252 364 }