PluginProbe
Image Optimization – Compress Images and Convert to WebP or AVIF / 1.1.0
Image Optimization – Compress Images and Convert to WebP or AVIF v1.1.0
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 1.6.6 All 32 releases
image-optimization / modules / optimization / classes / optimize-image.php

optimize-image.php in Image Optimization – Compress Images and Convert to WebP or AVIF 1.1.0, at modules/optimization/classes/optimize-image.php

344 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ImageOptimization\Modules\Optimization\Classes;
4
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,
12 Exceptions\Invalid_Image_Exception,
13 Image,
14 Image_Backup,
15 Image_DB_Update,
16 Image_Meta,
17 Image_Status,
18 WP_Image_Meta
19 };
20 use ImageOptimization\Classes\Logger;
21 use ImageOptimization\Classes\Utils;
22 use ImageOptimization\Modules\Oauth\Classes\Exceptions\Quota_Exceeded_Error;
23 use ImageOptimization\Modules\Oauth\Components\Connect;
24 use ImageOptimization\Modules\Optimization\Classes\{
25 Exceptions\Image_File_Already_Exists_Error,
26 Exceptions\Image_Optimization_Error,
27 Exceptions\Bulk_Token_Expired_Error,
28 Exceptions\Image_Already_Optimized_Error,
29 };
30 use ImageOptimization\Modules\Settings\Classes\Settings;
31 use Throwable;
32
33 if ( ! defined( 'ABSPATH' ) ) {
34 exit; // Exit if accessed directly.
35 }
36
37 /**
38 * The class is responsible for the optimization logic itself. It gets an image file, runs
39 * backup process if needed, sends a file to the service, stores the result and updates image meta.
40 *
41 * This class is used by manual, bulk and on-upload optimization flows.
42 */
43 class Optimize_Image {
44 private const IMAGE_OPTIMIZE_ENDPOINT = 'image/optimize';
45
46 protected ?Image $image;
47 protected WP_Image_Meta $wp_meta;
48 protected string $initiator;
49 protected ?string $bulk_token;
50 private string $current_image_path;
51 private string $current_image_size;
52 private bool $convert_to_webp;
53 private bool $keep_backups;
54 private array $current_size_duplicates;
55
56 /**
57 * @throws Quota_Exceeded_Error|Bulk_Token_Expired_Error|Image_File_Already_Exists_Error|Image_Optimization_Error
58 */
59 public function optimize(): void {
60 $sizes_enabled = Settings::get( Settings::CUSTOM_SIZES_OPTION_NAME );
61 $sizes_exist = $this->wp_meta->get_size_keys();
62
63 foreach ( $sizes_exist as $size_exist ) {
64 // If some image sizes optimization is disabled in settings, we check if the current one is still enabled
65 if (
66 'all' !== $sizes_enabled &&
67 Image::SIZE_FULL !== $size_exist &&
68 ! in_array( $size_exist, $sizes_enabled, true )
69 ) {
70 continue;
71 }
72
73 $image_meta = new Image_Meta( $this->image->get_id() );
74
75 // If the current size was already optimized -- ignore it.
76 if ( in_array( $size_exist, $image_meta->get_optimized_sizes(), true ) ) {
77 continue;
78 }
79
80 if ( ! file_exists( $this->image->get_file_path( $size_exist ) ) ) {
81 throw new Image_Optimization_Error( esc_html__( 'File is missing. Verify the upload', 'image-optimization' ) );
82 }
83
84 $this->current_image_size = $size_exist;
85 $this->current_size_duplicates = $this->wp_meta->get_size_duplicates( $size_exist );
86 $this->current_image_path = $this->image->get_file_path( $size_exist );
87
88 $this->optimize_current_size();
89
90 $this->current_image_size = '';
91 $this->current_size_duplicates = [];
92 $this->current_image_path = '';
93 }
94
95 $this->mark_as_optimized();
96
97 if ( ! $this->keep_backups ) {
98 Image_Backup::remove( $this->image->get_id() );
99 }
100 }
101
102 private function optimize_current_size(): void {
103 try {
104 $original_path = $this->current_image_path;
105 $response = $this->send_file();
106
107 $optimized_size = (int) $response->optimizedSize; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
108 $optimized_image_binary = base64_decode( $response->image, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
109
110 $this->replace_image_file( $optimized_image_binary );
111 $this->update_attachment_meta( $optimized_size );
112
113 if ( $original_path !== $this->current_image_path ) {
114 $this->update_posts( $original_path, $this->current_image_path );
115 }
116
117 // This should only be updated after meta
118 $this->update_attachment_post();
119 } catch ( Image_Already_Optimized_Error $iao ) {
120 // If we can't optimize it further, just file update the meta
121 $original_size = $this->wp_meta->get_file_size( $this->current_image_size )
122 ?? File_System::size( $this->image->get_file_path( $this->current_image_size ) );
123
124 $this->update_attachment_meta( $original_size );
125 $this->update_attachment_post();
126 } catch ( Bulk_Token_Expired_Error |Quota_Exceeded_Error |Image_File_Already_Exists_Error $e ) {
127 throw $e;
128 } catch ( Throwable $t ) {
129 // In case of anything else
130 throw new Image_Optimization_Error( $t->getMessage() );
131 }
132 }
133
134 private function send_file() {
135 $connect_status = Connect::check_connect_status();
136 $headers = [
137 'access_token' => $connect_status->access_token ?? '',
138 ];
139
140 if ( $this->bulk_token ) {
141 $headers['x-elementor-bulk-token'] = $this->bulk_token;
142 }
143
144 $optimization_options = [
145 'compression_level' => Settings::get( Settings::COMPRESSION_LEVEL_OPTION_NAME ),
146 'convert_to_webp' => $this->convert_to_webp,
147 'strip_exif' => Settings::get( Settings::STRIP_EXIF_METADATA_OPTION_NAME ),
148 ];
149
150 if ( Settings::get( Settings::RESIZE_LARGER_IMAGES_OPTION_NAME ) ) {
151 $optimization_options['resize'] = Settings::get( Settings::RESIZE_LARGER_IMAGES_SIZE_OPTION_NAME );
152 }
153
154 $image_key = wp_generate_password( 15, false );
155
156 $response = Utils::get_api_client()->make_request(
157 'POST',
158 self::IMAGE_OPTIMIZE_ENDPOINT,
159 [
160 'initiator' => $this->initiator,
161 'image_url' => $this->image->get_url( $this->current_image_size ),
162 'image_key' => $image_key,
163 'checksum' => md5_file( $this->current_image_path ),
164 'attachment_id' => $this->image->get_id(),
165 'attachment_parent_id' => Image::SIZE_FULL === $this->current_image_size ? 0 : $this->image->get_id(),
166 'image_optimization_settings' => base64_encode( wp_json_encode( $optimization_options ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
167 ],
168 $headers,
169 $this->current_image_path,
170 'image'
171 );
172
173 if ( ! isset( $response->imageKey ) || $image_key !== $response->imageKey ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
174 Logger::log(
175 Logger::LEVEL_ERROR,
176 "Image key must be $image_key, instead got $response->imageKey"
177 );
178
179 throw new Image_Optimization_Error( esc_html__( 'Service response is incorrect', 'image-optimization' ) );
180 }
181
182 $received_file_hash = md5( base64_decode( $response->image, true ) );
183
184 if ( ! isset( $response->checksum ) || $received_file_hash !== $response->checksum ) {
185 Logger::log(
186 Logger::LEVEL_ERROR,
187 "Image key must be $response->checksum, instead calculated $received_file_hash"
188 );
189
190 throw new Image_Optimization_Error( esc_html__( 'Service response is incorrect', 'image-optimization' ) );
191 }
192
193 return $response;
194 }
195
196 /**
197 * @throws File_System_Operation_Error|Image_Backup_Creation_Error|Image_File_Already_Exists_Error
198 */
199 private function replace_image_file( string $file_data ): void {
200 $path = $this->current_image_path;
201
202 // If we have backups disabled, we want to make sure we can download and save new file before we
203 // remove an existing one.
204 $tmp_path = File_Utils::replace_extension( $path, 'tmp' );
205
206 File_System::put_contents( $tmp_path, $file_data );
207
208 if ( $this->convert_to_webp ) {
209 $webp_path = File_Utils::replace_extension( $tmp_path, 'webp' );
210 $original_file_is_webp = strtolower( $path ) === strtolower( $webp_path );
211
212 if ( ! $original_file_is_webp && File_System::exists( $webp_path ) ) {
213 throw new Image_File_Already_Exists_Error();
214 }
215
216 // We want to keep both original as a fallback and optimized webp to prevent 404s
217 if ( ! $original_file_is_webp ) {
218 $this->keep_backups = true;
219
220 ( new Image_Meta( $this->image->get_id() ) )
221 ->set_image_backup_path( $this->current_image_size, $this->current_image_path )
222 ->save();
223 }
224
225 if ( $original_file_is_webp ) {
226 Image_Backup::create( $this->image->get_id(), $this->current_image_size, $this->current_image_path );
227 }
228
229 File_System::move( $tmp_path, $webp_path, true );
230
231 $path = $webp_path;
232 } else {
233 Image_Backup::create( $this->image->get_id(), $this->current_image_size, $this->current_image_path );
234
235 File_System::move( $tmp_path, $path, true );
236 }
237
238 if ( Image::SIZE_FULL === $this->current_image_size ) {
239 // Drop WP caches
240 update_attached_file( $this->image->get_id(), $path );
241 }
242
243 // Updating to the correct value
244 $this->current_image_path = $path;
245 }
246
247 /**
248 * Updates attachment records in the `wp_posts` table.
249 *
250 * @return void
251 */
252 private function update_attachment_post() {
253 $update_query = [];
254
255 if ( $this->convert_to_webp ) {
256 $attachment_object = $this->image->get_attachment_object();
257
258 $update_query['guid'] = File_Utils::replace_extension( $attachment_object->guid, 'webp' );
259 $update_query['post_mime_type'] = 'image/webp';
260 }
261
262 $update_query['post_modified'] = current_time( 'mysql' );
263 $update_query['post_modified_gmt'] = current_time( 'mysql', true );
264
265 $this->image->update_attachment( $update_query );
266 }
267
268 /**
269 * Updates attachment records in the `wp_postmeta` table.
270 *
271 * @param int $optimized_size
272 *
273 * @return void
274 */
275 private function update_attachment_meta( int $optimized_size ) {
276 $meta = new Image_Meta( $this->image->get_id() );
277
278 list($width, $height) = getimagesize( $this->current_image_path );
279
280 $sizes_to_update = [ $this->current_image_size, ...$this->current_size_duplicates ];
281
282 foreach ( $sizes_to_update as $size ) {
283 $meta
284 ->set_compression_level( Settings::get( Settings::COMPRESSION_LEVEL_OPTION_NAME ) )
285 ->add_optimized_size( $size )
286 ->add_original_data( $size, $this->wp_meta->get_size_data( $size ) );
287
288 $this->wp_meta
289 ->set_width( $size, $width )
290 ->set_height( $size, $height )
291 ->set_file_size( $size, $optimized_size );
292
293 if ( $this->convert_to_webp ) {
294 $this->wp_meta
295 ->set_file_path( $size, $this->current_image_path )
296 ->set_mime_type( $size, 'image/webp' );
297 }
298 }
299
300 $meta->save();
301 $this->wp_meta->save();
302 }
303
304 /**
305 * If we change an image extension, we should walk through the wp_posts table and update all the
306 * hardcoded image links to prevent 404s.
307 *
308 * @param string $old_path Previous image path
309 * @param string $new_path Current image path
310 *
311 * @return void
312 */
313 private function update_posts( string $old_path, string $new_path ) {
314 Image_DB_Update::update_posts_table_urls(
315 File_Utils::get_url_from_path( $old_path ),
316 File_Utils::get_url_from_path( $new_path )
317 );
318 }
319
320 /**
321 * Changes image status after all image sizes were optimized.
322 *
323 * @return void
324 */
325 private function mark_as_optimized() {
326 ( new Image_Meta( $this->image->get_id() ) )
327 ->set_status( Image_Status::OPTIMIZED )
328 ->set_error_type( null )
329 ->save();
330 }
331
332 /**
333 * @throws Invalid_Image_Exception
334 */
335 public function __construct( int $image_id, string $initiator, ?string $bulk_token = null ) {
336 $this->image = new Image( $image_id );
337 $this->wp_meta = new WP_Image_Meta( $image_id, $this->image );
338 $this->initiator = $initiator;
339 $this->bulk_token = $bulk_token;
340 $this->convert_to_webp = Settings::get( Settings::CONVERT_TO_WEBP_OPTION_NAME );
341 $this->keep_backups = Settings::get( Settings::BACKUP_ORIGINAL_IMAGES_OPTION_NAME );
342 }
343 }
344