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

262 lines 8.1 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 if ( $response->get_error_message() === 'Image already optimized' ) {
96 $original_size = $this->wp_meta->get_file_size( $this->current_image_size );
97
98 $this->update_attachment_meta( $original_size );
99 $this->update_attachment_post();
100
101 return;
102 }
103
104 throw new Image_Optimization_Error( $response->get_error_message() );
105 }
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 // This should only be updated after meta
114 $this->update_attachment_post();
115 }
116
117 private function send_file() {
118 $connect_status = Connect::check_connect_status();
119 $headers = [
120 'access_token' => $connect_status->access_token ?? '',
121 ];
122
123 if ( $this->bulk_token ) {
124 $headers['x-elementor-bulk-token'] = $this->bulk_token;
125 }
126
127 $optimization_options = [
128 'compression_level' => Settings::get( Settings::COMPRESSION_LEVEL_OPTION_NAME ),
129 'convert_to_webp' => $this->convert_to_webp,
130 'strip_exif' => Settings::get( Settings::STRIP_EXIF_METADATA_OPTION_NAME ),
131 ];
132
133 if ( Settings::get( Settings::RESIZE_LARGER_IMAGES_OPTION_NAME ) ) {
134 $optimization_options['resize'] = Settings::get( Settings::RESIZE_LARGER_IMAGES_SIZE_OPTION_NAME );
135 }
136
137 return Utils::get_api_client()->make_request(
138 'POST',
139 self::IMAGE_OPTIMIZE_ENDPOINT,
140 [
141 'initiator' => $this->initiator,
142 'image_url' => $this->image->get_url( $this->current_image_size ),
143 'attachment_id' => $this->image->get_id(),
144 'attachment_parent_id' => Image::SIZE_FULL === $this->current_image_size ? 0 : $this->image->get_id(),
145 'image_optimization_settings' => base64_encode( wp_json_encode( $optimization_options ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
146 ],
147 $headers,
148 $this->current_image_path,
149 'image'
150 );
151 }
152
153 private function replace_image_file( string $file_data ): void {
154 global $wp_filesystem;
155
156 $path = $this->current_image_path;
157
158 // If we have backups disabled, we want to make sure we can download and save new file before we
159 // remove an existing one.
160 $tmp_path = File_Utils::replace_extension( $path, 'tmp' );
161
162 $wp_filesystem->put_contents( $tmp_path, $file_data );
163
164 // The original file doesn't exist after any of these operations.
165 if ( Settings::get( Settings::BACKUP_ORIGINAL_IMAGES_OPTION_NAME ) ) {
166 Image_Backup::create( $this->image->get_id(), $this->current_image_size, $this->current_image_path );
167 } else {
168 $wp_filesystem->delete( $path, false, 'f' );
169 }
170
171 if ( $this->convert_to_webp ) {
172 $path = File_Utils::replace_extension( $tmp_path, 'webp' );
173 }
174
175 $wp_filesystem->move( $tmp_path, $path, true );
176
177 if ( Image::SIZE_FULL === $this->current_image_size ) {
178 // Drop WP caches
179 update_attached_file( $this->image->get_id(), $path );
180 }
181
182 // Updating to the correct value
183 $this->current_image_path = $path;
184 }
185
186 /**
187 * Updates attachment records in the `wp_posts` table.
188 *
189 * @return void
190 */
191 private function update_attachment_post() {
192 $update_query = [];
193
194 if ( $this->convert_to_webp ) {
195 $attachment_object = $this->image->get_attachment_object();
196
197 $update_query['guid'] = File_Utils::replace_extension( $attachment_object->guid, 'webp' );
198 $update_query['post_mime_type'] = 'image/webp';
199 }
200
201 $update_query['post_modified'] = current_time( 'mysql' );
202 $update_query['post_modified_gmt'] = current_time( 'mysql', true );
203
204 $this->image->update_attachment( $update_query );
205 }
206
207 /**
208 * Updates attachment records in the `wp_postmeta` table.
209 *
210 * @param int $optimized_size
211 *
212 * @return void
213 */
214 private function update_attachment_meta( int $optimized_size ) {
215 $meta = new Image_Meta( $this->image->get_id() );
216
217 list($width, $height) = getimagesize( $this->current_image_path );
218
219 $meta
220 ->set_compression_level( Settings::get( Settings::COMPRESSION_LEVEL_OPTION_NAME ) )
221 ->add_optimized_size( $this->current_image_size )
222 ->add_original_data( $this->current_image_size, $this->wp_meta->get_size_data( $this->current_image_size ) );
223
224 $this->wp_meta
225 ->set_width( $this->current_image_size, $width )
226 ->set_height( $this->current_image_size, $height )
227 ->set_file_size( $this->current_image_size, $optimized_size );
228
229 if ( $this->convert_to_webp ) {
230 $this->wp_meta
231 ->set_file_path( $this->current_image_size, $this->current_image_path )
232 ->set_mime_type( $this->current_image_size, 'image/webp' );
233 }
234
235 $meta->save();
236 $this->wp_meta->save();
237 }
238
239 /**
240 * Changes image status after all image sizes were optimized.
241 *
242 * @return void
243 */
244 private function mark_as_optimized() {
245 ( new Image_Meta( $this->image->get_id() ) )
246 ->set_status( Image_Status::OPTIMIZED )
247 ->set_error_type( null )
248 ->save();
249 }
250
251 /**
252 * @throws Invalid_Image_Exception
253 */
254 public function __construct( int $image_id, string $initiator, ?string $bulk_token = null ) {
255 $this->image = new Image( $image_id );
256 $this->wp_meta = new WP_Image_Meta( $image_id, $this->image );
257 $this->initiator = $initiator;
258 $this->bulk_token = $bulk_token;
259 $this->convert_to_webp = Settings::get( Settings::CONVERT_TO_WEBP_OPTION_NAME );
260 }
261 }
262