PluginProbe
Image Optimization – Compress Images and Convert to WebP or AVIF / 1.6.6
Image Optimization – Compress Images and Convert to WebP or AVIF v1.6.6
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 / stats / classes / optimization-stats.php

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

327 lines 9.3 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\Stats\Classes;
4
5 use ImageOptimization\Classes\Image\{
6 Image,
7 Image_Meta,
8 Image_Query_Builder,
9 Image_Status,
10 WP_Image_Meta,
11 Exceptions\Invalid_Image_Exception
12 };
13 use ImageOptimization\Classes\Async_Operation\{
14 Async_Operation,
15 Async_Operation_Hook,
16 Async_Operation_Queue,
17 Exceptions\Async_Operation_Exception,
18 Queries\Operation_Query,
19 };
20 use ImageOptimization\Classes\File_System\{
21 Exceptions\File_System_Operation_Error,
22 File_System,
23 };
24 use ImageOptimization\Classes\Logger;
25 use ImageOptimization\Modules\Optimization\Classes\Exceptions\Image_Validation_Error;
26 use ImageOptimization\Modules\Optimization\Classes\Validate_Image;
27 use ImageOptimization\Modules\Settings\Classes\Settings;
28
29 if ( ! defined( 'ABSPATH' ) ) {
30 exit; // Exit if accessed directly.
31 }
32
33 class Optimization_Stats {
34 const PAGING_SIZE = 25000;
35 const STATS_CALCULATION_DELAY = 60 * 15;
36
37 /**
38 * Returns image stats.
39 * If the library is too big, it queries images in chunks.
40 *
41 * @return array{total_image_count: int, optimized_image_count: int, current_image_size: int, initial_image_size:
42 * int}
43 */
44 public static function get_image_stats( ?int $image_id = null, ?bool $no_cache = false ): array {
45 // No caching needed if we check a specific image.
46 if ( $image_id ) {
47 $output = self::get_image_stats_chunk( 1, $image_id );
48
49 unset( $output['pages'] );
50
51 return $output;
52 }
53
54 // Look at the stored data first. If it's still valid -- use it.
55 $stats = self::get_stored_stats();
56
57 if ( ! $no_cache && ( time() - $stats['updated_at'] ) <= self::STATS_CALCULATION_DELAY ) {
58 unset( $stats['updated_at'] );
59
60 return $stats;
61 }
62
63 // Otherwise, recalculate the stats.
64 $output = self::get_image_stats_chunk( 1, $image_id );
65 $pages_count = $output['pages'];
66
67 if ( $pages_count <= 1 ) {
68 unset( $output['pages'] );
69
70 return $output;
71 }
72
73 if ( $pages_count > 1 && Async_Operation::OPERATION_STATUS_NOT_STARTED === self::get_stats_calculation_status() ) {
74 try {
75 Async_Operation::create(
76 Async_Operation_Hook::CALCULATE_OPTIMIZATION_STATS,
77 [
78 'page' => 2,
79 'pages_count' => $pages_count,
80 'output' => $output,
81 ],
82 Async_Operation_Queue::STATS
83 );
84 } catch ( Async_Operation_Exception $aoe ) {
85 Logger::log( Logger::LEVEL_ERROR, 'Error while creating a stats calculation task: ' . $aoe->getMessage() );
86 }
87 }
88
89 unset( $stats['updated_at'] );
90
91 return $stats;
92 }
93
94 /**
95 * @return array{pages: int, total_image_count: int, optimized_image_count: int, current_image_size: int,
96 * initial_image_size: int}
97 */
98 public static function get_image_stats_chunk( int $paged = 1, ?int $image_id = null ): array {
99 $output = [
100 'pages' => 1,
101 'total_image_count' => 0,
102 'optimized_image_count' => 0,
103 'current_image_size' => 0,
104 'initial_image_size' => 0,
105 ];
106
107 $query = ( new Image_Query_Builder() )
108 ->set_paging_size( self::PAGING_SIZE )
109 ->set_current_page( $paged );
110
111 if ( $image_id ) {
112 $query->set_image_ids( [ $image_id ] );
113 }
114
115 $query = $query->execute();
116
117 $output['pages'] = (int) $query->max_num_pages;
118
119 foreach ( $query->posts as $attachment_id ) {
120 try {
121 Validate_Image::is_valid( $attachment_id );
122
123 $image = new Image( $attachment_id );
124 $wp_meta = new WP_Image_Meta( $attachment_id );
125 } catch ( Invalid_Image_Exception |Image_Validation_Error $ie ) {
126 continue;
127 }
128
129 $meta = new Image_Meta( $attachment_id );
130 $image_sizes = $wp_meta->get_size_keys();
131
132 foreach ( $image_sizes as $image_size ) {
133 if ( ! $image->file_exists( $image_size ) ) {
134 continue;
135 }
136
137 $output['total_image_count']++;
138
139 $output['current_image_size'] += self::calculate_current_image_file_size( $attachment_id, $wp_meta, $image_size );
140 $output['initial_image_size'] += self::calculate_initial_image_file_size( $attachment_id, $meta, $wp_meta, $image_size );
141 }
142
143 $optimized_sizes = self::filter_only_enabled_sizes( $meta->get_optimized_sizes() );
144 $output['optimized_image_count'] += count( $optimized_sizes );
145 }
146
147 return $output;
148 }
149
150 public static function get_optimization_details( int $attachment_id ): array {
151 try {
152 $wp_meta = new WP_Image_Meta( $attachment_id );
153 } catch ( Invalid_Image_Exception $iie ) {
154 Logger::log( Logger::LEVEL_ERROR, "No metadata found for image ID: {$attachment_id}" );
155
156 throw $iie;
157 }
158
159 $output = [
160 'total' => 0,
161 'sizes' => [],
162 ];
163
164 $meta = new Image_Meta( $attachment_id );
165 $image_sizes = $wp_meta->get_size_keys();
166 $sizes_enabled = Settings::get( Settings::CUSTOM_SIZES_OPTION_NAME );
167
168 foreach ( $image_sizes as $image_size ) {
169 if (
170 'all' !== $sizes_enabled &&
171 Image::SIZE_FULL !== $image_size &&
172 ! in_array( $image_size, $sizes_enabled, true )
173 ) {
174 continue;
175 }
176
177 $current_image_size = self::calculate_current_image_file_size( $attachment_id, $wp_meta, $image_size );
178 $output['total'] += $current_image_size;
179
180 $status = self::get_image_size_optimization_status( $attachment_id, $image_size );
181
182 if ( Image_Status::OPTIMIZED === $status ) {
183 $dimensions_updated = $wp_meta->get_width( $image_size ) !== (int) $meta->get_original_width( $image_size ) ||
184 $wp_meta->get_height( $image_size ) !== (int) $meta->get_original_height( $image_size );
185
186 $new_dimensions = $dimensions_updated ? [
187 'width' => $wp_meta->get_width( $image_size ),
188 'height' => $wp_meta->get_height( $image_size ),
189 ] : null;
190
191 $initial_image_size = self::calculate_initial_image_file_size( $attachment_id, $meta, $wp_meta, $image_size );
192
193 $saved = [
194 'relative' => max( 100 - round( $current_image_size / $initial_image_size * 100 ), 0 ),
195 'absolute' => $initial_image_size - $current_image_size,
196 ];
197 }
198
199 $output['sizes'][] = [
200 'size_name' => $image_size,
201 'file_size' => $current_image_size,
202 'status' => self::get_image_size_optimization_status( $attachment_id, $image_size ),
203 'saved' => $saved ?? null,
204 'new_dimensions' => $new_dimensions ?? null,
205 ];
206 }
207
208 return $output;
209 }
210
211 private static function get_image_size_optimization_status( int $attachment_id, string $size_name ): string {
212 $image = new Image( $attachment_id );
213 $meta = new Image_Meta( $attachment_id );
214
215 if ( in_array( $size_name, $meta->get_optimized_sizes(), true ) ) {
216 return Image_Status::OPTIMIZED;
217 }
218
219 if ( ! $image->file_exists( $size_name ) ) {
220 return 'file-not-found';
221 }
222
223 try {
224 Validate_Image::validate_file_size( $attachment_id, $size_name );
225 } catch ( Image_Validation_Error $ive ) {
226 return 'file-too-large';
227 }
228
229 return Image_Status::NOT_OPTIMIZED;
230 }
231
232 private static function calculate_current_image_file_size( int $image_id, WP_Image_Meta $wp_meta, string $image_size ): int {
233 $size_from_meta = $wp_meta->get_file_size( $image_size );
234
235 if ( $size_from_meta ) {
236 return $size_from_meta;
237 }
238
239 try {
240 return File_System::size( ( new Image( $image_id ) )->get_file_path( $image_size ) );
241 } catch ( File_System_Operation_Error $e ) {
242 return 0;
243 }
244 }
245
246 private static function calculate_initial_image_file_size( int $image_id, Image_Meta $meta, WP_Image_Meta $wp_meta, string $image_size ): int {
247 $size_from_meta = $meta->get_original_file_size( $image_size ) ?? $wp_meta->get_file_size( $image_size );
248
249 if ( $size_from_meta ) {
250 return $size_from_meta;
251 }
252
253 try {
254 return File_System::size( ( new Image( $image_id ) )->get_file_path( $image_size ) );
255 } catch ( File_System_Operation_Error $e ) {
256 return 0;
257 }
258 }
259
260 private static function filter_only_enabled_sizes( array $size_keys ): array {
261 $enabled_sizes = Settings::get( Settings::CUSTOM_SIZES_OPTION_NAME );
262
263 if ( 'all' === $enabled_sizes ) {
264 return array_filter( $size_keys, fn( string $size_key ) => ! str_starts_with( $size_key, 'elementor_' ) );
265 }
266
267 return array_filter($size_keys, function( string $size ) use ( $enabled_sizes ) {
268 if ( Image::SIZE_FULL === $size ) {
269 return true;
270 }
271
272 if ( in_array( $size, $enabled_sizes, true ) ) {
273 return true;
274 }
275
276 return false;
277 });
278 }
279
280 /**
281 * Retrieves stats data.
282 *
283 * @return array{total_image_count: ?int, optimized_image_count: ?int, current_image_size: ?int, initial_image_size:
284 * ?int, updated_at: ?int}
285 */
286 public static function get_stored_stats(): array {
287 $default = [
288 'total_image_count' => null,
289 'optimized_image_count' => null,
290 'current_image_size' => null,
291 'initial_image_size' => null,
292 'updated_at' => null,
293 ];
294
295 return json_decode( get_option( 'image_optimizer_optimization_stats', json_encode( $default ) ), ARRAY_A );
296 }
297
298 /**
299 * Updates the optimization stats with fresh values.
300 *
301 * @param array $stats
302 *
303 * @return bool
304 */
305 public static function set_stored_stats( array $stats ) {
306 $stats['updated_at'] = time();
307
308 return update_option( 'image_optimizer_optimization_stats', json_encode( $stats ) );
309 }
310
311 private static function get_stats_calculation_status(): string {
312 $active_query = ( new Operation_Query() )
313 ->set_hook( Async_Operation_Hook::CALCULATE_OPTIMIZATION_STATS )
314 ->set_status( [
315 Async_Operation::OPERATION_STATUS_PENDING,
316 Async_Operation::OPERATION_STATUS_RUNNING,
317 ] )
318 ->set_limit( 1 );
319
320 $active_operations = Async_Operation::get( $active_query );
321
322 return ! empty( $active_operations )
323 ? Async_Operation::OPERATION_STATUS_RUNNING
324 : Async_Operation::OPERATION_STATUS_NOT_STARTED;
325 }
326 }
327