PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.65
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.65
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Image_Optimizer / Image_Optimizer.php

Image_Optimizer.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.65, at includes/extensions/Image_Optimizer/Image_Optimizer.php

681 lines 25.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Image Optimizer extension.
4 *
5 * Unlimited Image Optimizer - Browser-based image conversion to WebP (Canvas API).
6 *
7 * @package King_Addons
8 */
9
10 namespace King_Addons\Image_Optimizer;
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 require_once __DIR__ . '/Image_Optimizer_DB.php';
17 require_once __DIR__ . '/Image_Optimizer_Ajax.php';
18
19 class Image_Optimizer
20 {
21 private const OPTION_NAME = 'king_addons_image_optimizer_settings';
22 private const META_KEY = '_king_img_optimized';
23
24 private const FREE_MONTHLY_OPTIMIZATION_LIMIT = 200;
25 private const FREE_QUOTA_OPTION = 'king_addons_img_opt_free_quota';
26
27 private static ?Image_Optimizer $instance = null;
28
29 /**
30 * Cached settings.
31 *
32 * @var array<string, mixed>
33 */
34 private array $settings = [];
35
36 public static function instance(): Image_Optimizer
37 {
38 if (self::$instance === null) {
39 self::$instance = new self();
40 }
41
42 return self::$instance;
43 }
44
45 private function __construct()
46 {
47 $this->settings = $this->get_settings();
48
49 add_action('admin_menu', [$this, 'register_admin_menu']);
50 add_action('admin_init', [$this, 'register_settings']);
51 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
52
53 // Initialize AJAX handlers
54 Image_Optimizer_Ajax::instance();
55
56 // Add media library column
57 add_filter('manage_media_columns', [$this, 'add_media_column']);
58 add_action('manage_media_custom_column', [$this, 'render_media_column'], 10, 2);
59
60 // Add attachment meta box
61 add_action('add_meta_boxes_attachment', [$this, 'add_attachment_metabox']);
62
63 // Attachment Details panel (upload.php?item=ID) + Media modal
64 add_filter('attachment_fields_to_edit', [$this, 'add_attachment_details_fields'], 10, 2);
65
66 // Clean up WebP files when attachment is deleted
67 add_action('delete_attachment', [$this, 'cleanup_on_delete']);
68 }
69
70 /**
71 * Add Image Optimizer details to the Attachment Details screen and media modal.
72 *
73 * @param array $form_fields
74 * @param \WP_Post $post
75 * @return array
76 */
77 public function add_attachment_details_fields(array $form_fields, \WP_Post $post): array
78 {
79 if (!wp_attachment_is_image($post->ID)) {
80 return $form_fields;
81 }
82
83 $form_fields['king_img_optimizer'] = [
84 'label' => esc_html__('Image Optimizer', 'king-addons'),
85 'input' => 'html',
86 'html' => $this->get_attachment_optimizer_card_html((int) $post->ID),
87 ];
88
89 return $form_fields;
90 }
91
92 /**
93 * Build the "King Image Optimizer" card HTML for Attachment Details / media modal.
94 */
95 public function get_attachment_optimizer_card_html(int $attachment_id): string
96 {
97 if (!wp_attachment_is_image($attachment_id)) {
98 return '';
99 }
100
101 $file = get_attached_file($attachment_id);
102 $original_bytes_current = (!empty($file) && is_string($file) && file_exists($file)) ? (int) filesize($file) : 0;
103
104 $meta = Image_Optimizer_DB::get_optimization_meta($attachment_id) ?: [];
105 $is_optimized = (($meta['status'] ?? '') === 'optimized');
106
107 $original_bytes = $original_bytes_current;
108 $optimized_bytes = 0;
109 $saved_bytes = 0;
110 $saved_percent = 0;
111
112 if ($is_optimized) {
113 $full = is_array($meta['sizes']['full'] ?? null) ? $meta['sizes']['full'] : [];
114 $original_path = $full['original_path'] ?? '';
115 $optimized_path = $full['optimized_path'] ?? ($full['webp_path'] ?? '');
116
117 if (!empty($original_path) && is_string($original_path) && file_exists($original_path)) {
118 $original_bytes = (int) filesize($original_path);
119 } else {
120 $original_bytes = (int) ($full['original_size'] ?? ($meta['total_original_bytes'] ?? $original_bytes));
121 }
122
123 if (!empty($optimized_path) && is_string($optimized_path) && file_exists($optimized_path)) {
124 $optimized_bytes = (int) filesize($optimized_path);
125 } else {
126 $optimized_bytes = (int) ($full['optimized_size'] ?? ($meta['total_optimized_bytes'] ?? 0));
127 }
128
129 $saved_bytes = max(0, $original_bytes - $optimized_bytes);
130 $saved_percent = $original_bytes > 0 ? round(($saved_bytes / $original_bytes) * 100, 1) : 0;
131 }
132
133 $btn_label = $is_optimized
134 ? esc_html__('Restore Original', 'king-addons')
135 : esc_html__('Optimize Image', 'king-addons');
136
137 $btn_class = $is_optimized ? 'button' : 'button button-primary';
138 $btn_action = $is_optimized ? 'restore' : 'convert';
139
140 $status_badge = $is_optimized
141 ? '<span class="king-img-attach-badge king-img-attach-badge--optimized">' . esc_html__('Optimized', 'king-addons') . '</span>'
142 : '<span class="king-img-attach-badge king-img-attach-badge--pending">' . esc_html__('Not optimized', 'king-addons') . '</span>';
143
144 $rows_html = '';
145 $rows_html .= '<div class="king-img-attach-row"><span class="king-img-attach-k">' . esc_html__('Original', 'king-addons') . '</span><span class="king-img-attach-v">' . esc_html(self::format_bytes($original_bytes)) . '</span></div>';
146
147 if ($is_optimized) {
148 $rows_html .= '<div class="king-img-attach-row"><span class="king-img-attach-k">' . esc_html__('Optimized', 'king-addons') . '</span><span class="king-img-attach-v">' . esc_html(self::format_bytes($optimized_bytes)) . '</span></div>';
149 $rows_html .= '<div class="king-img-attach-row"><span class="king-img-attach-k">' . esc_html__('Saved', 'king-addons') . '</span><span class="king-img-attach-v king-img-attach-v--good">' . esc_html(self::format_bytes($saved_bytes)) . ' (' . esc_html($saved_percent) . '%)</span></div>';
150 }
151
152 $html = '';
153 $html .= '<div class="king-img-attach-card" data-king-img-attachment-id="' . esc_attr((string) $attachment_id) . '" data-king-img-status="' . esc_attr($is_optimized ? 'optimized' : 'pending') . '">';
154 $html .= '<div class="king-img-attach-head">';
155 $html .= '<div class="king-img-attach-title">' . esc_html__('King Image Optimizer', 'king-addons') . '</div>';
156 $html .= '<div class="king-img-attach-badges">' . $status_badge . '</div>';
157 $html .= '</div>';
158 $html .= '<div class="king-img-attach-body">' . $rows_html . '</div>';
159 $html .= '<div class="king-img-attach-actions">';
160 $html .= '<button type="button" class="' . esc_attr($btn_class) . ' king-img-attachment-action" data-king-img-action="' . esc_attr($btn_action) . '">' . esc_html($btn_label) . '</button>';
161 $html .= '<span class="spinner king-img-attach-spinner" style="float:none;"></span>';
162 $html .= '<span class="king-img-attach-msg" aria-live="polite"></span>';
163 $html .= '</div>';
164 $html .= '</div>';
165
166 return $html;
167 }
168
169 /**
170 * Check if Pro version is available.
171 */
172 public function is_pro(): bool
173 {
174 // If the PRO plugin is active, it defines these constants.
175 if (defined('KING_ADDONS_PRO_PATH') || defined('KING_ADDONS_PRO_VERSION')) {
176 return true;
177 }
178
179 // License-based check (Freemius).
180 if (function_exists('king_addons_freemius')) {
181 $fs = king_addons_freemius();
182 if (is_object($fs) && method_exists($fs, 'can_use_premium_code__premium_only')) {
183 return (bool) $fs->can_use_premium_code__premium_only();
184 }
185 }
186
187 // Backward-compatible helper.
188 return function_exists('king_addons_can_use_pro') && king_addons_can_use_pro();
189 }
190
191 /**
192 * Get monthly free quota state.
193 *
194 * @return array{limit:int, used:int, remaining:int, month_key:string}
195 */
196 public function get_free_quota_state(): array
197 {
198 $limit = (int) self::FREE_MONTHLY_OPTIMIZATION_LIMIT;
199 $month_key = wp_date('Y-m', (int) current_time('timestamp'));
200
201 $stored = get_option(self::FREE_QUOTA_OPTION, []);
202 $stored = is_array($stored) ? $stored : [];
203 $stored_month = (string) ($stored['month_key'] ?? '');
204 $used = (int) ($stored['used'] ?? 0);
205
206 if ($stored_month !== $month_key || $used < 0) {
207 $used = 0;
208 update_option(self::FREE_QUOTA_OPTION, [
209 'month_key' => $month_key,
210 'used' => 0,
211 ]);
212 }
213
214 $remaining = max(0, $limit - $used);
215
216 return [
217 'limit' => $limit,
218 'used' => $used,
219 'remaining' => $remaining,
220 'month_key' => $month_key,
221 ];
222 }
223
224 /**
225 * Consume free quota.
226 *
227 * @return array{limit:int, used:int, remaining:int, month_key:string}
228 */
229 public function consume_free_quota(int $amount = 1): array
230 {
231 $amount = max(0, (int) $amount);
232 if ($amount === 0) {
233 return $this->get_free_quota_state();
234 }
235
236 $state = $this->get_free_quota_state();
237 $new_used = (int) $state['used'] + $amount;
238
239 update_option(self::FREE_QUOTA_OPTION, [
240 'month_key' => (string) $state['month_key'],
241 'used' => $new_used,
242 ]);
243
244 return $this->get_free_quota_state();
245 }
246
247 /**
248 * Upgrade URL used across the plugin.
249 */
250 public function get_upgrade_url(string $source = 'kng-img-optimizer'): string
251 {
252 $source = sanitize_key($source);
253 return 'https://kingaddons.com/pricing/?utm_source=' . rawurlencode($source) . '&utm_medium=wp-admin&utm_campaign=kng';
254 }
255
256 /**
257 * Get default settings.
258 */
259 public function get_default_settings(): array
260 {
261 return [
262 'quality' => 82,
263 'auto_replace_urls' => true,
264 'auto_optimize_uploads' => false,
265 'skip_small' => false,
266 'min_size' => 10240, // 10KB
267 'resize_enabled' => true,
268 'max_width' => 2048,
269 'create_backups' => true,
270 ];
271 }
272
273 /**
274 * Get current settings.
275 */
276 public function get_settings(): array
277 {
278 $saved = get_option(self::OPTION_NAME, []);
279 return wp_parse_args($saved, $this->get_default_settings());
280 }
281
282 /**
283 * Save settings.
284 */
285 public function save_settings(array $settings): bool
286 {
287 $sanitized = [
288 'quality' => max(1, min(100, absint($settings['quality'] ?? 82))),
289 'auto_replace_urls' => !empty($settings['auto_replace_urls']),
290 'auto_optimize_uploads' => $this->is_pro() && !empty($settings['auto_optimize_uploads']),
291 'skip_small' => !empty($settings['skip_small']),
292 'min_size' => absint($settings['min_size'] ?? 10240),
293 'resize_enabled' => !empty($settings['resize_enabled']),
294 'max_width' => max(100, min(5000, absint($settings['max_width'] ?? 2048))),
295 'create_backups' => !empty($settings['create_backups']),
296 ];
297
298 $this->settings = $sanitized;
299 return update_option(self::OPTION_NAME, $sanitized);
300 }
301
302 /**
303 * Register admin menu.
304 */
305 public function register_admin_menu(): void
306 {
307 add_submenu_page(
308 'king-addons',
309 esc_html__('Image Optimizer', 'king-addons'),
310 esc_html__('Image Optimizer', 'king-addons'),
311 'manage_options',
312 'king-addons-image-optimizer',
313 [$this, 'render_admin_page']
314 );
315 }
316
317 /**
318 * Register settings.
319 */
320 public function register_settings(): void
321 {
322 register_setting('king_addons_image_optimizer', self::OPTION_NAME, [
323 'type' => 'array',
324 'sanitize_callback' => [$this, 'sanitize_settings'],
325 ]);
326 }
327
328 /**
329 * Sanitize settings callback.
330 */
331 public function sanitize_settings($input): array
332 {
333 return [
334 'quality' => max(1, min(100, absint($input['quality'] ?? 82))),
335 'auto_replace_urls' => !empty($input['auto_replace_urls']),
336 'auto_optimize_uploads' => !empty($input['auto_optimize_uploads']),
337 'skip_small' => !empty($input['skip_small']),
338 'min_size' => absint($input['min_size'] ?? 10240),
339 'resize_enabled' => !empty($input['resize_enabled']),
340 'max_width' => max(100, min(5000, absint($input['max_width'] ?? 2048))),
341 'create_backups' => !empty($input['create_backups']),
342 ];
343 }
344
345 /**
346 * Enqueue admin assets.
347 */
348 public function enqueue_admin_assets(string $hook): void
349 {
350 $assets_url = KING_ADDONS_URL . 'includes/extensions/Image_Optimizer/assets/';
351 $assets_path = KING_ADDONS_PATH . 'includes/extensions/Image_Optimizer/assets/';
352
353 $screen = function_exists('get_current_screen') ? get_current_screen() : null;
354 $is_optimizer_page = ($hook === 'king-addons_page_king-addons-image-optimizer');
355 $is_media_library = ($hook === 'upload.php');
356 $is_attachment_edit = (
357 ($hook === 'post.php' || $hook === 'post-new.php')
358 && $screen
359 && ($screen->post_type ?? '') === 'attachment'
360 );
361
362 $can_use_media = current_user_can('upload_files');
363
364 // Attachment Details / media modal additions
365 if ($can_use_media) {
366 wp_enqueue_style(
367 'king-addons-image-optimizer-attachment-details',
368 $assets_url . 'attachment-details.css',
369 [],
370 file_exists($assets_path . 'attachment-details.css') ? filemtime($assets_path . 'attachment-details.css') : KING_ADDONS_VERSION
371 );
372
373 wp_enqueue_script(
374 'king-addons-image-optimizer-attachment-details',
375 $assets_url . 'attachment-details.js',
376 ['jquery'],
377 file_exists($assets_path . 'attachment-details.js') ? filemtime($assets_path . 'attachment-details.js') : KING_ADDONS_VERSION,
378 true
379 );
380
381 wp_localize_script('king-addons-image-optimizer-attachment-details', 'kingImageOptimizerAttachment', [
382 'ajaxUrl' => admin_url('admin-ajax.php'),
383 'nonce' => wp_create_nonce('king_img_optimizer_nonce'),
384 'settings' => $this->settings,
385 'isPro' => $this->is_pro(),
386 'quota' => $this->get_free_quota_state(),
387 'upgradeUrl' => $this->get_upgrade_url('kng-img-optimizer'),
388 'strings' => [
389 'fetching' => esc_html__('Fetching image data...', 'king-addons'),
390 'converting' => esc_html__('Converting to WebP...', 'king-addons'),
391 'restoring' => esc_html__('Restoring original...', 'king-addons'),
392 'optimized' => esc_html__('Optimized', 'king-addons'),
393 'restored' => esc_html__('Restored', 'king-addons'),
394 'skipped' => esc_html__('Skipped (too small)', 'king-addons'),
395 'doneReload' => esc_html__('Done. Reloading...', 'king-addons'),
396 'confirmRestore' => esc_html__('Restore original and delete optimized files?', 'king-addons'),
397 'errorPrefix' => esc_html__('Error: ', 'king-addons'),
398 'quotaExceeded' => esc_html__('Free plan limit reached (200 optimizations/month). Upgrade to Unlimited to continue.', 'king-addons'),
399 ],
400 ]);
401 }
402
403 // Only load the full optimizer UI assets on our admin page
404 if (!$is_optimizer_page) {
405 return;
406 }
407
408 // Admin styles
409 wp_enqueue_style(
410 'king-addons-image-optimizer',
411 $assets_url . 'admin.css',
412 [],
413 file_exists($assets_path . 'admin.css') ? filemtime($assets_path . 'admin.css') : KING_ADDONS_VERSION
414 );
415
416 // Shared V3 styles
417 wp_enqueue_style(
418 'king-addons-admin-v3',
419 KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css',
420 [],
421 KING_ADDONS_VERSION
422 );
423
424 // Optimizer script
425 wp_enqueue_script(
426 'king-addons-image-optimizer',
427 $assets_url . 'optimizer.js',
428 ['jquery'],
429 file_exists($assets_path . 'optimizer.js') ? filemtime($assets_path . 'optimizer.js') : KING_ADDONS_VERSION,
430 true
431 );
432
433 // Bulk optimizer script
434 wp_enqueue_script(
435 'king-addons-bulk-optimizer',
436 $assets_url . 'bulk-optimizer.js',
437 ['jquery', 'king-addons-image-optimizer'],
438 file_exists($assets_path . 'bulk-optimizer.js') ? filemtime($assets_path . 'bulk-optimizer.js') : KING_ADDONS_VERSION,
439 true
440 );
441
442 // Localize script
443 wp_localize_script('king-addons-image-optimizer', 'kingImageOptimizer', [
444 'ajaxUrl' => admin_url('admin-ajax.php'),
445 'nonce' => wp_create_nonce('king_img_optimizer_nonce'),
446 'settings' => $this->settings,
447 'isPro' => $this->is_pro(),
448 'quota' => $this->get_free_quota_state(),
449 'upgradeUrl' => $this->get_upgrade_url('kng-img-optimizer'),
450 'strings' => [
451 'optimizing' => esc_html__('Optimizing...', 'king-addons'),
452 'optimized' => esc_html__('Optimized', 'king-addons'),
453 'error' => esc_html__('Error', 'king-addons'),
454 'pending' => esc_html__('Pending', 'king-addons'),
455 'skipped' => esc_html__('Skipped', 'king-addons'),
456 'confirmRestore' => esc_html__('Are you sure you want to restore the original image?', 'king-addons'),
457 'confirmBulkRestore' => esc_html__('Are you sure you want to restore all images to their original versions?', 'king-addons'),
458 'leaveWarning' => esc_html__('A process is running. If you leave this page, it will be interrupted. Do you want to leave?', 'king-addons'),
459 'processingImage' => esc_html__('Processing image %d of %d...', 'king-addons'),
460 'completed' => esc_html__('Optimization completed!', 'king-addons'),
461 'savedBytes' => esc_html__('Saved %s', 'king-addons'),
462 'quotaExceeded' => esc_html__('Free plan limit reached (200 optimizations/month). Upgrade to Unlimited to continue.', 'king-addons'),
463 ],
464 ]);
465 }
466
467 /**
468 * Render admin page.
469 */
470 public function render_admin_page(): void
471 {
472 include __DIR__ . '/templates/admin-page.php';
473 }
474
475 /**
476 * Add media library column.
477 */
478 public function add_media_column(array $columns): array
479 {
480 $columns['king_optimization'] = esc_html__('Optimization', 'king-addons');
481 return $columns;
482 }
483
484 /**
485 * Render media library column.
486 */
487 public function render_media_column(string $column_name, int $attachment_id): void
488 {
489 if ($column_name !== 'king_optimization') {
490 return;
491 }
492
493 if (!wp_attachment_is_image($attachment_id)) {
494 echo '<span class="king-opt-na">—</span>';
495 return;
496 }
497
498 $meta = Image_Optimizer_DB::get_optimization_meta($attachment_id);
499
500 if (empty($meta) || $meta['status'] === 'pending') {
501 echo '<span class="king-opt-pending"><span class="dashicons dashicons-clock" aria-hidden="true"></span> ' . esc_html__('Pending', 'king-addons') . '</span>';
502 } elseif ($meta['status'] === 'optimized') {
503 $savings = $meta['total_saved_bytes'] ?? 0;
504 $percent = $meta['savings_percent'] ?? 0;
505 echo '<span class="king-opt-done"><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span> ' . esc_html(sprintf(__('-%d%%', 'king-addons'), $percent)) . '</span>';
506 } elseif ($meta['status'] === 'failed') {
507 echo '<span class="king-opt-failed"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span> ' . esc_html__('Failed', 'king-addons') . '</span>';
508 }
509 }
510
511 /**
512 * Add attachment metabox.
513 */
514 public function add_attachment_metabox(): void
515 {
516 add_meta_box(
517 'king_image_optimization',
518 esc_html__('Image Optimization', 'king-addons'),
519 [$this, 'render_attachment_metabox'],
520 'attachment',
521 'side',
522 'default'
523 );
524 }
525
526 /**
527 * Render attachment metabox.
528 */
529 public function render_attachment_metabox(\WP_Post $post): void
530 {
531 if (!wp_attachment_is_image($post->ID)) {
532 echo '<p>' . esc_html__('This file type is not supported for optimization.', 'king-addons') . '</p>';
533 return;
534 }
535
536 $meta = Image_Optimizer_DB::get_optimization_meta($post->ID);
537 $file = get_attached_file($post->ID);
538 $file_size = $file ? filesize($file) : 0;
539
540 include __DIR__ . '/templates/attachment-metabox.php';
541 }
542
543 /**
544 * Cleanup WebP files when attachment is deleted.
545 */
546 public function cleanup_on_delete(int $attachment_id): void
547 {
548 $meta = Image_Optimizer_DB::get_optimization_meta($attachment_id);
549
550 if (empty($meta) || empty($meta['sizes'])) {
551 return;
552 }
553
554 foreach ($meta['sizes'] as $size => $data) {
555 if (!empty($data['webp_path']) && file_exists($data['webp_path'])) {
556 wp_delete_file($data['webp_path']);
557 }
558 }
559
560 Image_Optimizer_DB::delete_optimization_meta($attachment_id);
561 }
562
563 /**
564 * Get global statistics.
565 */
566 public function get_global_stats(): array
567 {
568 global $wpdb;
569
570 $stats = [
571 'total_images' => 0,
572 'optimized_images' => 0,
573 'skipped_images' => 0,
574 'failed_images' => 0,
575 'pending_images' => 0,
576 'total_original_bytes' => 0,
577 'total_optimized_bytes' => 0,
578 'total_saved_bytes' => 0,
579 ];
580
581 // Count total images
582 $stats['total_images'] = (int) $wpdb->get_var(
583 "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_mime_type LIKE 'image/%'"
584 );
585
586 // Get optimization stats from postmeta
587 $optimized_data = $wpdb->get_results(
588 $wpdb->prepare(
589 "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm
590 INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
591 WHERE pm.meta_key = %s AND p.post_type = 'attachment'",
592 self::META_KEY
593 )
594 );
595
596 // Opportunistic migration: sync a small batch to Media Library per request
597 $sync_limit = 200;
598 $synced = 0;
599
600 foreach ($optimized_data as $row) {
601 $attachment_id = (int) ($row->post_id ?? 0);
602 $meta = maybe_unserialize($row->meta_value);
603 if (!is_array($meta)) {
604 continue;
605 }
606
607 if (($meta['status'] ?? '') === 'optimized') {
608 $stats['optimized_images']++;
609 $stats['total_original_bytes'] += $meta['total_original_bytes'] ?? 0;
610 $stats['total_optimized_bytes'] += $meta['total_optimized_bytes'] ?? 0;
611 $stats['total_saved_bytes'] += $meta['total_saved_bytes'] ?? 0;
612
613 // If Media Library still points to original, sync it (limited per call)
614 if ($synced < $sync_limit && $attachment_id > 0) {
615 $opt_full = $meta['sizes']['full']['optimized_path'] ?? $meta['sizes']['full']['webp_path'] ?? '';
616 if (!empty($opt_full) && is_string($opt_full) && file_exists($opt_full)) {
617 $current = get_attached_file($attachment_id);
618 if (!empty($current) && $current !== $opt_full) {
619 Image_Optimizer_DB::sync_attachment_to_optimized($attachment_id, $meta);
620 $synced++;
621 }
622 }
623 }
624 } elseif (($meta['status'] ?? '') === 'skipped') {
625 $stats['skipped_images']++;
626 } elseif (($meta['status'] ?? '') === 'failed') {
627 $stats['failed_images']++;
628 }
629 }
630
631 if ($synced > 0) {
632 $stats['synced_media_library'] = $synced;
633 }
634
635 // Pending = Total - Optimized - Skipped - Failed
636 $stats['pending_images'] = max(0, $stats['total_images'] - $stats['optimized_images'] - $stats['skipped_images'] - $stats['failed_images']);
637
638 return $stats;
639 }
640
641 /**
642 * Get supported image types.
643 */
644 public function get_supported_types(): array
645 {
646 $types = [
647 'image/jpeg' => ['ext' => 'jpg', 'label' => 'JPEG'],
648 'image/png' => ['ext' => 'png', 'label' => 'PNG'],
649 'image/webp' => ['ext' => 'webp', 'label' => 'WebP'],
650 ];
651 return $types;
652 }
653
654 /**
655 * Check if image type is supported.
656 */
657 public function is_supported_type(string $mime_type): bool
658 {
659 return array_key_exists($mime_type, $this->get_supported_types());
660 }
661
662 /**
663 * Format bytes to human readable.
664 */
665 public static function format_bytes(int $bytes, int $precision = 2): string
666 {
667 $units = ['B', 'KB', 'MB', 'GB', 'TB'];
668 $bytes = max($bytes, 0);
669 $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
670 $pow = min($pow, count($units) - 1);
671 $bytes /= pow(1024, $pow);
672
673 return round($bytes, $precision) . ' ' . $units[$pow];
674 }
675 }
676
677 // Initialize the extension
678 add_action('init', function() {
679 Image_Optimizer::instance();
680 });
681