PluginProbe
Image Optimizer – Compress Images and Convert to WebP or AVIF / 1.0.1
Image Optimizer – Compress Images and Convert to WebP or AVIF v1.0.1
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
image-optimization / modules / optimization / classes / optimize-image.php

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

259 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ImageOptimizer\Modules\Optimization\Classes;
4
5 use ImageOptimizer\Classes\File_Utils;
6 use ImageOptimizer\Classes\Image\{
7 Exceptions\Invalid_Image_Exception,
8 Image,
9 Image_Backup,
10 Image_Meta,
11 Image_Status,
12 WP_Image_Meta
13 };
14 use ImageOptimizer\Classes\Utils;
15 use ImageOptimizer\Modules\Oauth\Classes\Exceptions\Quota_Exceeded_Error;
16 use ImageOptimizer\Modules\Oauth\Components\Connect;
17 use ImageOptimizer\Modules\Optimization\Classes\Exceptions\Image_Optimization_Error;
18 use ImageOptimizer\Modules\Settings\Classes\Settings;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit; // Exit if accessed directly.
22 }
23
24 require_once ABSPATH . 'wp-admin/includes/file.php';
25 WP_Filesystem();
26
27 /**
28 * The class is responsible for the optimization logic itself. It gets an image file, runs
29 * backup process if needed, sends a file to the service, stores the result and updates image meta.
30 *
31 * This class is used by manual, bulk and on-upload optimization flows.
32 */
33 class Optimize_Image {
34 private const IMAGE_OPTIMIZE_ENDPOINT = 'image/optimize';
35
36 protected ?Image $image;
37 protected WP_Image_Meta $wp_meta;
38 protected string $initiator;
39 protected ?string $bulk_token;
40 private string $current_image_path;
41 private string $current_image_size;
42 private bool $convert_to_webp;
43
44 /**
45 * @throws Image_Optimization_Error|Quota_Exceeded_Error
46 */
47 public function optimize(): void {
48 $sizes_enabled = Settings::get( Settings::CUSTOM_SIZES_OPTION_NAME );
49 $sizes_exist = $this->wp_meta->get_size_keys();
50
51 foreach ( $sizes_exist as $size_exist ) {
52 // If some image sizes optimization is disabled in settings, we check if the current one is still enabled
53 if (
54 'all' !== $sizes_enabled &&
55 Image::SIZE_FULL !== $size_exist &&
56 ! in_array( $size_exist, $sizes_enabled, true )
57 ) {
58 continue;
59 }
60
61 $image_meta = new Image_Meta( $this->image->get_id() );
62
63 // If the current size was already optimized -- ignore it.
64 if ( in_array( $size_exist, $image_meta->get_optimized_sizes(), true ) ) {
65 continue;
66 }
67
68 if ( ! file_exists( $this->image->get_file_path( $size_exist ) ) ) {
69 throw new Image_Optimization_Error( esc_html__( 'File is missing. Verify the upload', 'image-optimizer' ) );
70 }
71
72 $this->current_image_size = $size_exist;
73 $this->current_image_path = $this->image->get_file_path( $size_exist );
74
75 $this->optimize_current_size();
76
77 $this->current_image_size = '';
78 $this->current_image_path = '';
79 }
80
81 $this->mark_as_optimized();
82 }
83
84 /**
85 * @throws Image_Optimization_Error|Quota_Exceeded_Error
86 */
87 private function optimize_current_size(): void {
88 $response = $this->send_file();
89
90 if ( is_wp_error( $response ) ) {
91 if ( $response->get_error_message() === 'user reached limit' ) {
92 throw new Quota_Exceeded_Error( esc_html__( 'Plan quota reached', 'image-optimizer' ) );
93 }
94
95 throw new Image_Optimization_Error( $response->get_error_message() );
96 }
97
98 $original_size = (int) $response->originalSize; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
99 $optimized_size = (int) $response->optimizedSize; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
100 $optimized_image_binary = base64_decode( $response->image, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
101
102 // Sometimes the BE returns images bigger than original ones. In that case, we don't want to replace the original.
103 if ( $original_size > $optimized_size ) {
104 $this->replace_image_file( $optimized_image_binary );
105 $this->update_attachment_meta( $optimized_size );
106 } else {
107 $this->update_attachment_meta( $original_size );
108 }
109
110 // This should only be updated after meta
111 $this->update_attachment_post();
112 }
113
114 private function send_file() {
115 $connect_status = Connect::check_connect_status();
116 $headers = [
117 'access_token' => $connect_status->access_token ?? '',
118 ];
119
120 if ( $this->bulk_token ) {
121 $headers['x-elementor-bulk-token'] = $this->bulk_token;
122 }
123
124 $optimization_options = [
125 'compression_level' => Settings::get( Settings::COMPRESSION_LEVEL_OPTION_NAME ),
126 'convert_to_webp' => $this->convert_to_webp,
127 'strip_exif' => Settings::get( Settings::STRIP_EXIF_METADATA_OPTION_NAME ),
128 ];
129
130 if ( Settings::get( Settings::RESIZE_LARGER_IMAGES_OPTION_NAME ) ) {
131 $optimization_options['resize'] = Settings::get( Settings::RESIZE_LARGER_IMAGES_SIZE_OPTION_NAME );
132 }
133
134 return Utils::get_api_client()->make_request(
135 'POST',
136 self::IMAGE_OPTIMIZE_ENDPOINT,
137 [
138 'initiator' => $this->initiator,
139 'image_url' => $this->image->get_url( $this->current_image_size ),
140 'attachment_id' => $this->image->get_id(),
141 'attachment_parent_id' => Image::SIZE_FULL === $this->current_image_size ? 0 : $this->image->get_id(),
142 'image_optimization_settings' => base64_encode( wp_json_encode( $optimization_options ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
143 ],
144 $headers,
145 $this->current_image_path,
146 'image'
147 );
148 }
149
150 private function replace_image_file( string $file_data ): void {
151 global $wp_filesystem;
152
153 $path = $this->current_image_path;
154
155 // If we have backups disabled, we want to make sure we can download and save new file before we
156 // remove an existing one.
157 $tmp_path = File_Utils::replace_extension( $path, 'tmp' );
158
159 $wp_filesystem->put_contents( $tmp_path, $file_data );
160
161 // The original file doesn't exist after any of these operations.
162 if ( Settings::get( Settings::BACKUP_ORIGINAL_IMAGES_OPTION_NAME ) ) {
163 Image_Backup::create( $this->image->get_id(), $this->current_image_size, $this->current_image_path );
164 } else {
165 $wp_filesystem->delete( $path, false, 'f' );
166 }
167
168 if ( $this->convert_to_webp ) {
169 $path = File_Utils::replace_extension( $tmp_path, 'webp' );
170 }
171
172 $wp_filesystem->move( $tmp_path, $path, true );
173
174 if ( Image::SIZE_FULL === $this->current_image_size ) {
175 // Drop WP caches
176 update_attached_file( $this->image->get_id(), $path );
177 }
178
179 // Updating to the correct value
180 $this->current_image_path = $path;
181 }
182
183 /**
184 * Updates attachment records in the `wp_posts` table.
185 *
186 * @return void
187 */
188 private function update_attachment_post() {
189 $update_query = [];
190
191 if ( $this->convert_to_webp ) {
192 $attachment_object = $this->image->get_attachment_object();
193
194 $update_query['guid'] = File_Utils::replace_extension( $attachment_object->guid, 'webp' );
195 $update_query['post_mime_type'] = 'image/webp';
196 }
197
198 $update_query['post_modified'] = current_time( 'mysql' );
199 $update_query['post_modified_gmt'] = current_time( 'mysql', true );
200
201 $this->image->update_attachment( $update_query );
202 }
203
204 /**
205 * Updates attachment records in the `wp_postmeta` table.
206 *
207 * @param int $optimized_size
208 *
209 * @return void
210 */
211 private function update_attachment_meta( int $optimized_size ) {
212 $meta = new Image_Meta( $this->image->get_id() );
213
214 list($width, $height) = getimagesize( $this->current_image_path );
215
216 $meta
217 ->set_compression_level( Settings::get( Settings::COMPRESSION_LEVEL_OPTION_NAME ) )
218 ->add_optimized_size( $this->current_image_size )
219 ->add_original_data( $this->current_image_size, $this->wp_meta->get_size_data( $this->current_image_size ) );
220
221 $this->wp_meta
222 ->set_width( $this->current_image_size, $width )
223 ->set_height( $this->current_image_size, $height )
224 ->set_file_size( $this->current_image_size, $optimized_size );
225
226 if ( $this->convert_to_webp ) {
227 $this->wp_meta
228 ->set_file_path( $this->current_image_size, $this->current_image_path )
229 ->set_mime_type( $this->current_image_size, 'image/webp' );
230 }
231
232 $meta->save();
233 $this->wp_meta->save();
234 }
235
236 /**
237 * Changes image status after all image sizes were optimized.
238 *
239 * @return void
240 */
241 private function mark_as_optimized() {
242 ( new Image_Meta( $this->image->get_id() ) )
243 ->set_status( Image_Status::OPTIMIZED )
244 ->set_error_type( null )
245 ->save();
246 }
247
248 /**
249 * @throws Invalid_Image_Exception
250 */
251 public function __construct( int $image_id, string $initiator, ?string $bulk_token = null ) {
252 $this->image = new Image( $image_id );
253 $this->wp_meta = new WP_Image_Meta( $image_id, $this->image );
254 $this->initiator = $initiator;
255 $this->bulk_token = $bulk_token;
256 $this->convert_to_webp = Settings::get( Settings::CONVERT_TO_WEBP_OPTION_NAME );
257 }
258 }
259