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

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

320 lines 9.1 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 = 2000;
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 = self::filter_only_enabled_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 = self::filter_only_enabled_sizes( $wp_meta->get_size_keys() );
166
167 foreach ( $image_sizes as $image_size ) {
168 $current_image_size = self::calculate_current_image_file_size( $attachment_id, $wp_meta, $image_size );
169 $output['total'] += $current_image_size;
170
171 $status = self::get_image_size_optimization_status( $attachment_id, $image_size );
172
173 if ( Image_Status::OPTIMIZED === $status ) {
174 $dimensions_updated = $wp_meta->get_width( $image_size ) !== (int) $meta->get_original_width( $image_size ) ||
175 $wp_meta->get_height( $image_size ) !== (int) $meta->get_original_height( $image_size );
176
177 $new_dimensions = $dimensions_updated ? [
178 'width' => $wp_meta->get_width( $image_size ),
179 'height' => $wp_meta->get_height( $image_size ),
180 ] : null;
181
182 $initial_image_size = self::calculate_initial_image_file_size( $attachment_id, $meta, $wp_meta, $image_size );
183
184 $saved = [
185 'relative' => max( 100 - round( $current_image_size / $initial_image_size * 100 ), 0 ),
186 'absolute' => $initial_image_size - $current_image_size,
187 ];
188 }
189
190 $output['sizes'][] = [
191 'size_name' => $image_size,
192 'file_size' => $current_image_size,
193 'status' => self::get_image_size_optimization_status( $attachment_id, $image_size ),
194 'saved' => $saved ?? null,
195 'new_dimensions' => $new_dimensions ?? null,
196 ];
197 }
198
199 return $output;
200 }
201
202 private static function get_image_size_optimization_status( int $attachment_id, string $size_name ): string {
203 $image = new Image( $attachment_id );
204 $meta = new Image_Meta( $attachment_id );
205
206 if ( in_array( $size_name, $meta->get_optimized_sizes(), true ) ) {
207 return Image_Status::OPTIMIZED;
208 }
209
210 if ( ! $image->file_exists( $size_name ) ) {
211 return 'file-not-found';
212 }
213
214 try {
215 Validate_Image::validate_file_size( $attachment_id, $size_name );
216 } catch ( Image_Validation_Error $ive ) {
217 return 'file-too-large';
218 }
219
220 return Image_Status::NOT_OPTIMIZED;
221 }
222
223 private static function calculate_current_image_file_size( int $image_id, WP_Image_Meta $wp_meta, string $image_size ): int {
224 $size_from_meta = $wp_meta->get_file_size( $image_size );
225
226 if ( $size_from_meta ) {
227 return $size_from_meta;
228 }
229
230 try {
231 return File_System::size( ( new Image( $image_id ) )->get_file_path( $image_size ) );
232 } catch ( File_System_Operation_Error $e ) {
233 return 0;
234 }
235 }
236
237 private static function calculate_initial_image_file_size( int $image_id, Image_Meta $meta, WP_Image_Meta $wp_meta, string $image_size ): int {
238 $size_from_meta = $meta->get_original_file_size( $image_size ) ?? $wp_meta->get_file_size( $image_size );
239
240 if ( $size_from_meta ) {
241 return $size_from_meta;
242 }
243
244 try {
245 return File_System::size( ( new Image( $image_id ) )->get_file_path( $image_size ) );
246 } catch ( File_System_Operation_Error $e ) {
247 return 0;
248 }
249 }
250
251 private static function filter_only_enabled_sizes( array $size_keys ): array {
252 $enabled_sizes = Settings::get( Settings::CUSTOM_SIZES_OPTION_NAME );
253
254 if ( 'all' === $enabled_sizes ) {
255 return array_filter( $size_keys, fn( string $size_key ) => ! str_starts_with( $size_key, 'elementor_' ) );
256 }
257
258 return array_filter($size_keys, function( string $size ) use ( $enabled_sizes ) {
259 if ( Image::SIZE_FULL === $size ) {
260 return true;
261 }
262
263 if ( in_array( $size, $enabled_sizes, true ) ) {
264 return true;
265 }
266
267 return false;
268 });
269 }
270
271 /**
272 * Retrieves stats data.
273 *
274 * @return array{total_image_count: ?int, optimized_image_count: ?int, current_image_size: ?int, initial_image_size:
275 * ?int, updated_at: ?int}
276 */
277 public static function get_stored_stats(): array {
278 $default = [
279 'total_image_count' => null,
280 'optimized_image_count' => null,
281 'current_image_size' => null,
282 'initial_image_size' => null,
283 'updated_at' => null,
284 ];
285
286 $output = json_decode( get_option( 'image_optimizer_optimization_stats' ), ARRAY_A );
287
288 return is_array( $output ) ? $output : $default;
289 }
290
291 /**
292 * Updates the optimization stats with fresh values.
293 *
294 * @param array $stats
295 *
296 * @return bool
297 */
298 public static function set_stored_stats( array $stats ): bool {
299 $stats['updated_at'] = time();
300
301 return update_option( 'image_optimizer_optimization_stats', json_encode( $stats ) );
302 }
303
304 private static function get_stats_calculation_status(): string {
305 $active_query = ( new Operation_Query() )
306 ->set_hook( Async_Operation_Hook::CALCULATE_OPTIMIZATION_STATS )
307 ->set_status( [
308 Async_Operation::OPERATION_STATUS_PENDING,
309 Async_Operation::OPERATION_STATUS_RUNNING,
310 ] )
311 ->set_limit( 1 );
312
313 $active_operations = Async_Operation::get( $active_query );
314
315 return ! empty( $active_operations )
316 ? Async_Operation::OPERATION_STATUS_RUNNING
317 : Async_Operation::OPERATION_STATUS_NOT_STARTED;
318 }
319 }
320