*/
private array $settings = [];
public static function instance(): Image_Optimizer
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct()
{
$this->settings = $this->get_settings();
add_action('admin_menu', [$this, 'register_admin_menu']);
add_action('admin_init', [$this, 'register_settings']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
// Initialize AJAX handlers
Image_Optimizer_Ajax::instance();
// Add media library column
add_filter('manage_media_columns', [$this, 'add_media_column']);
add_action('manage_media_custom_column', [$this, 'render_media_column'], 10, 2);
// Add attachment meta box
add_action('add_meta_boxes_attachment', [$this, 'add_attachment_metabox']);
// Attachment Details panel (upload.php?item=ID) + Media modal
add_filter('attachment_fields_to_edit', [$this, 'add_attachment_details_fields'], 10, 2);
// Clean up WebP files when attachment is deleted
add_action('delete_attachment', [$this, 'cleanup_on_delete']);
}
/**
* Add Image Optimizer details to the Attachment Details screen and media modal.
*
* @param array $form_fields
* @param \WP_Post $post
* @return array
*/
public function add_attachment_details_fields(array $form_fields, \WP_Post $post): array
{
if (!wp_attachment_is_image($post->ID)) {
return $form_fields;
}
$form_fields['king_img_optimizer'] = [
'label' => esc_html__('Image Optimizer', 'king-addons'),
'input' => 'html',
'html' => $this->get_attachment_optimizer_card_html((int) $post->ID),
];
return $form_fields;
}
/**
* Build the "King Image Optimizer" card HTML for Attachment Details / media modal.
*/
public function get_attachment_optimizer_card_html(int $attachment_id): string
{
if (!wp_attachment_is_image($attachment_id)) {
return '';
}
$file = get_attached_file($attachment_id);
$original_bytes_current = (!empty($file) && is_string($file) && file_exists($file)) ? (int) filesize($file) : 0;
$meta = Image_Optimizer_DB::get_optimization_meta($attachment_id) ?: [];
$is_optimized = (($meta['status'] ?? '') === 'optimized');
$original_bytes = $original_bytes_current;
$optimized_bytes = 0;
$saved_bytes = 0;
$saved_percent = 0;
if ($is_optimized) {
$full = is_array($meta['sizes']['full'] ?? null) ? $meta['sizes']['full'] : [];
$original_path = $full['original_path'] ?? '';
$optimized_path = $full['optimized_path'] ?? ($full['webp_path'] ?? '');
if (!empty($original_path) && is_string($original_path) && file_exists($original_path)) {
$original_bytes = (int) filesize($original_path);
} else {
$original_bytes = (int) ($full['original_size'] ?? ($meta['total_original_bytes'] ?? $original_bytes));
}
if (!empty($optimized_path) && is_string($optimized_path) && file_exists($optimized_path)) {
$optimized_bytes = (int) filesize($optimized_path);
} else {
$optimized_bytes = (int) ($full['optimized_size'] ?? ($meta['total_optimized_bytes'] ?? 0));
}
$saved_bytes = max(0, $original_bytes - $optimized_bytes);
$saved_percent = $original_bytes > 0 ? round(($saved_bytes / $original_bytes) * 100, 1) : 0;
}
$btn_label = $is_optimized
? esc_html__('Restore Original', 'king-addons')
: esc_html__('Optimize Image', 'king-addons');
$btn_class = $is_optimized ? 'button' : 'button button-primary';
$btn_action = $is_optimized ? 'restore' : 'convert';
$status_badge = $is_optimized
? '' . esc_html__('Optimized', 'king-addons') . ''
: '' . esc_html__('Not optimized', 'king-addons') . '';
$rows_html = '';
$rows_html .= '
' . esc_html__('Original', 'king-addons') . '' . esc_html(self::format_bytes($original_bytes)) . '
';
if ($is_optimized) {
$rows_html .= '' . esc_html__('Optimized', 'king-addons') . '' . esc_html(self::format_bytes($optimized_bytes)) . '
';
$rows_html .= '' . esc_html__('Saved', 'king-addons') . '' . esc_html(self::format_bytes($saved_bytes)) . ' (' . esc_html($saved_percent) . '%)
';
}
$html = '';
$html .= '';
$html .= '
';
$html .= '
' . esc_html__('King Image Optimizer', 'king-addons') . '
';
$html .= '
' . $status_badge . '
';
$html .= '
';
$html .= '
' . $rows_html . '
';
$html .= '
';
$html .= '';
$html .= '';
$html .= '';
$html .= '
';
$html .= '
';
return $html;
}
/**
* Check if Pro version is available.
*/
public function is_pro(): bool
{
// If the PRO plugin is active, it defines these constants.
if (defined('KING_ADDONS_PRO_PATH') || defined('KING_ADDONS_PRO_VERSION')) {
return true;
}
// License-based check (Freemius).
if (function_exists('king_addons_freemius')) {
$fs = king_addons_freemius();
if (is_object($fs) && method_exists($fs, 'can_use_premium_code__premium_only')) {
return (bool) $fs->can_use_premium_code__premium_only();
}
}
// Backward-compatible helper.
return function_exists('king_addons_can_use_pro') && king_addons_can_use_pro();
}
/**
* Get monthly free quota state.
*
* @return array{limit:int, used:int, remaining:int, month_key:string}
*/
public function get_free_quota_state(): array
{
$limit = (int) self::FREE_MONTHLY_OPTIMIZATION_LIMIT;
$month_key = wp_date('Y-m', (int) current_time('timestamp'));
$stored = get_option(self::FREE_QUOTA_OPTION, []);
$stored = is_array($stored) ? $stored : [];
$stored_month = (string) ($stored['month_key'] ?? '');
$used = (int) ($stored['used'] ?? 0);
if ($stored_month !== $month_key || $used < 0) {
$used = 0;
update_option(self::FREE_QUOTA_OPTION, [
'month_key' => $month_key,
'used' => 0,
]);
}
$remaining = max(0, $limit - $used);
return [
'limit' => $limit,
'used' => $used,
'remaining' => $remaining,
'month_key' => $month_key,
];
}
/**
* Consume free quota.
*
* @return array{limit:int, used:int, remaining:int, month_key:string}
*/
public function consume_free_quota(int $amount = 1): array
{
$amount = max(0, (int) $amount);
if ($amount === 0) {
return $this->get_free_quota_state();
}
$state = $this->get_free_quota_state();
$new_used = (int) $state['used'] + $amount;
update_option(self::FREE_QUOTA_OPTION, [
'month_key' => (string) $state['month_key'],
'used' => $new_used,
]);
return $this->get_free_quota_state();
}
/**
* Upgrade URL used across the plugin.
*/
public function get_upgrade_url(string $source = 'kng-img-optimizer'): string
{
$source = sanitize_key($source);
return 'https://kingaddons.com/pricing/?utm_source=' . rawurlencode($source) . '&utm_medium=wp-admin&utm_campaign=kng';
}
/**
* Get default settings.
*/
public function get_default_settings(): array
{
return [
'quality' => 82,
'auto_replace_urls' => true,
'auto_optimize_uploads' => false,
'skip_small' => false,
'min_size' => 10240, // 10KB
'resize_enabled' => true,
'max_width' => 2048,
'create_backups' => true,
];
}
/**
* Get current settings.
*/
public function get_settings(): array
{
$saved = get_option(self::OPTION_NAME, []);
return wp_parse_args($saved, $this->get_default_settings());
}
/**
* Save settings.
*/
public function save_settings(array $settings): bool
{
$sanitized = [
'quality' => max(1, min(100, absint($settings['quality'] ?? 82))),
'auto_replace_urls' => !empty($settings['auto_replace_urls']),
'auto_optimize_uploads' => $this->is_pro() && !empty($settings['auto_optimize_uploads']),
'skip_small' => !empty($settings['skip_small']),
'min_size' => absint($settings['min_size'] ?? 10240),
'resize_enabled' => !empty($settings['resize_enabled']),
'max_width' => max(100, min(5000, absint($settings['max_width'] ?? 2048))),
'create_backups' => !empty($settings['create_backups']),
];
$this->settings = $sanitized;
return update_option(self::OPTION_NAME, $sanitized);
}
/**
* Register admin menu.
*/
public function register_admin_menu(): void
{
add_submenu_page(
'king-addons',
esc_html__('Image Optimizer', 'king-addons'),
esc_html__('Image Optimizer', 'king-addons'),
'manage_options',
'king-addons-image-optimizer',
[$this, 'render_admin_page']
);
}
/**
* Register settings.
*/
public function register_settings(): void
{
register_setting('king_addons_image_optimizer', self::OPTION_NAME, [
'type' => 'array',
'sanitize_callback' => [$this, 'sanitize_settings'],
]);
}
/**
* Sanitize settings callback.
*/
public function sanitize_settings($input): array
{
return [
'quality' => max(1, min(100, absint($input['quality'] ?? 82))),
'auto_replace_urls' => !empty($input['auto_replace_urls']),
'auto_optimize_uploads' => !empty($input['auto_optimize_uploads']),
'skip_small' => !empty($input['skip_small']),
'min_size' => absint($input['min_size'] ?? 10240),
'resize_enabled' => !empty($input['resize_enabled']),
'max_width' => max(100, min(5000, absint($input['max_width'] ?? 2048))),
'create_backups' => !empty($input['create_backups']),
];
}
/**
* Enqueue admin assets.
*/
public function enqueue_admin_assets(string $hook): void
{
$assets_url = KING_ADDONS_URL . 'includes/extensions/Image_Optimizer/assets/';
$assets_path = KING_ADDONS_PATH . 'includes/extensions/Image_Optimizer/assets/';
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
$is_optimizer_page = ($hook === 'king-addons_page_king-addons-image-optimizer');
$is_media_library = ($hook === 'upload.php');
$is_attachment_edit = (
($hook === 'post.php' || $hook === 'post-new.php')
&& $screen
&& ($screen->post_type ?? '') === 'attachment'
);
$can_use_media = current_user_can('upload_files');
// Attachment Details / media modal additions
if ($can_use_media) {
wp_enqueue_style(
'king-addons-image-optimizer-attachment-details',
$assets_url . 'attachment-details.css',
[],
file_exists($assets_path . 'attachment-details.css') ? filemtime($assets_path . 'attachment-details.css') : KING_ADDONS_VERSION
);
wp_enqueue_script(
'king-addons-image-optimizer-attachment-details',
$assets_url . 'attachment-details.js',
['jquery'],
file_exists($assets_path . 'attachment-details.js') ? filemtime($assets_path . 'attachment-details.js') : KING_ADDONS_VERSION,
true
);
wp_localize_script('king-addons-image-optimizer-attachment-details', 'kingImageOptimizerAttachment', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('king_img_optimizer_nonce'),
'settings' => $this->settings,
'isPro' => $this->is_pro(),
'quota' => $this->get_free_quota_state(),
'upgradeUrl' => $this->get_upgrade_url('kng-img-optimizer'),
'strings' => [
'fetching' => esc_html__('Fetching image data...', 'king-addons'),
'converting' => esc_html__('Converting to WebP...', 'king-addons'),
'restoring' => esc_html__('Restoring original...', 'king-addons'),
'optimized' => esc_html__('Optimized', 'king-addons'),
'restored' => esc_html__('Restored', 'king-addons'),
'skipped' => esc_html__('Skipped (too small)', 'king-addons'),
'doneReload' => esc_html__('Done. Reloading...', 'king-addons'),
'confirmRestore' => esc_html__('Restore original and delete optimized files?', 'king-addons'),
'errorPrefix' => esc_html__('Error: ', 'king-addons'),
'quotaExceeded' => esc_html__('Free plan limit reached (200 optimizations/month). Upgrade to Unlimited to continue.', 'king-addons'),
],
]);
}
// Only load the full optimizer UI assets on our admin page
if (!$is_optimizer_page) {
return;
}
// Admin styles
wp_enqueue_style(
'king-addons-image-optimizer',
$assets_url . 'admin.css',
[],
file_exists($assets_path . 'admin.css') ? filemtime($assets_path . 'admin.css') : KING_ADDONS_VERSION
);
// Shared V3 styles
wp_enqueue_style(
'king-addons-admin-v3',
KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css',
[],
KING_ADDONS_VERSION
);
// Optimizer script
wp_enqueue_script(
'king-addons-image-optimizer',
$assets_url . 'optimizer.js',
['jquery'],
file_exists($assets_path . 'optimizer.js') ? filemtime($assets_path . 'optimizer.js') : KING_ADDONS_VERSION,
true
);
// Bulk optimizer script
wp_enqueue_script(
'king-addons-bulk-optimizer',
$assets_url . 'bulk-optimizer.js',
['jquery', 'king-addons-image-optimizer'],
file_exists($assets_path . 'bulk-optimizer.js') ? filemtime($assets_path . 'bulk-optimizer.js') : KING_ADDONS_VERSION,
true
);
// Localize script
wp_localize_script('king-addons-image-optimizer', 'kingImageOptimizer', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('king_img_optimizer_nonce'),
'settings' => $this->settings,
'isPro' => $this->is_pro(),
'quota' => $this->get_free_quota_state(),
'upgradeUrl' => $this->get_upgrade_url('kng-img-optimizer'),
'strings' => [
'optimizing' => esc_html__('Optimizing...', 'king-addons'),
'optimized' => esc_html__('Optimized', 'king-addons'),
'error' => esc_html__('Error', 'king-addons'),
'pending' => esc_html__('Pending', 'king-addons'),
'skipped' => esc_html__('Skipped', 'king-addons'),
'confirmRestore' => esc_html__('Are you sure you want to restore the original image?', 'king-addons'),
'confirmBulkRestore' => esc_html__('Are you sure you want to restore all images to their original versions?', 'king-addons'),
'leaveWarning' => esc_html__('A process is running. If you leave this page, it will be interrupted. Do you want to leave?', 'king-addons'),
'processingImage' => esc_html__('Processing image %d of %d...', 'king-addons'),
'completed' => esc_html__('Optimization completed!', 'king-addons'),
'savedBytes' => esc_html__('Saved %s', 'king-addons'),
'quotaExceeded' => esc_html__('Free plan limit reached (200 optimizations/month). Upgrade to Unlimited to continue.', 'king-addons'),
],
]);
}
/**
* Render admin page.
*/
public function render_admin_page(): void
{
include __DIR__ . '/templates/admin-page.php';
}
/**
* Add media library column.
*/
public function add_media_column(array $columns): array
{
$columns['king_optimization'] = esc_html__('Optimization', 'king-addons');
return $columns;
}
/**
* Render media library column.
*/
public function render_media_column(string $column_name, int $attachment_id): void
{
if ($column_name !== 'king_optimization') {
return;
}
if (!wp_attachment_is_image($attachment_id)) {
echo '—';
return;
}
$meta = Image_Optimizer_DB::get_optimization_meta($attachment_id);
if (empty($meta) || $meta['status'] === 'pending') {
echo ' ' . esc_html__('Pending', 'king-addons') . '';
} elseif ($meta['status'] === 'optimized') {
$savings = $meta['total_saved_bytes'] ?? 0;
$percent = $meta['savings_percent'] ?? 0;
echo ' ' . esc_html(sprintf(__('-%d%%', 'king-addons'), $percent)) . '';
} elseif ($meta['status'] === 'failed') {
echo ' ' . esc_html__('Failed', 'king-addons') . '';
}
}
/**
* Add attachment metabox.
*/
public function add_attachment_metabox(): void
{
add_meta_box(
'king_image_optimization',
esc_html__('Image Optimization', 'king-addons'),
[$this, 'render_attachment_metabox'],
'attachment',
'side',
'default'
);
}
/**
* Render attachment metabox.
*/
public function render_attachment_metabox(\WP_Post $post): void
{
if (!wp_attachment_is_image($post->ID)) {
echo '' . esc_html__('This file type is not supported for optimization.', 'king-addons') . '
';
return;
}
$meta = Image_Optimizer_DB::get_optimization_meta($post->ID);
$file = get_attached_file($post->ID);
$file_size = $file ? filesize($file) : 0;
include __DIR__ . '/templates/attachment-metabox.php';
}
/**
* Cleanup WebP files when attachment is deleted.
*/
public function cleanup_on_delete(int $attachment_id): void
{
$meta = Image_Optimizer_DB::get_optimization_meta($attachment_id);
if (empty($meta) || empty($meta['sizes'])) {
return;
}
foreach ($meta['sizes'] as $size => $data) {
if (!empty($data['webp_path']) && file_exists($data['webp_path'])) {
wp_delete_file($data['webp_path']);
}
}
Image_Optimizer_DB::delete_optimization_meta($attachment_id);
}
/**
* Get global statistics.
*/
public function get_global_stats(): array
{
global $wpdb;
$stats = [
'total_images' => 0,
'optimized_images' => 0,
'skipped_images' => 0,
'failed_images' => 0,
'pending_images' => 0,
'total_original_bytes' => 0,
'total_optimized_bytes' => 0,
'total_saved_bytes' => 0,
];
// Count total images
$stats['total_images'] = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_mime_type LIKE 'image/%'"
);
// Get optimization stats from postmeta
$optimized_data = $wpdb->get_results(
$wpdb->prepare(
"SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
WHERE pm.meta_key = %s AND p.post_type = 'attachment'",
self::META_KEY
)
);
// Opportunistic migration: sync a small batch to Media Library per request
$sync_limit = 200;
$synced = 0;
foreach ($optimized_data as $row) {
$attachment_id = (int) ($row->post_id ?? 0);
$meta = maybe_unserialize($row->meta_value);
if (!is_array($meta)) {
continue;
}
if (($meta['status'] ?? '') === 'optimized') {
$stats['optimized_images']++;
$stats['total_original_bytes'] += $meta['total_original_bytes'] ?? 0;
$stats['total_optimized_bytes'] += $meta['total_optimized_bytes'] ?? 0;
$stats['total_saved_bytes'] += $meta['total_saved_bytes'] ?? 0;
// If Media Library still points to original, sync it (limited per call)
if ($synced < $sync_limit && $attachment_id > 0) {
$opt_full = $meta['sizes']['full']['optimized_path'] ?? $meta['sizes']['full']['webp_path'] ?? '';
if (!empty($opt_full) && is_string($opt_full) && file_exists($opt_full)) {
$current = get_attached_file($attachment_id);
if (!empty($current) && $current !== $opt_full) {
Image_Optimizer_DB::sync_attachment_to_optimized($attachment_id, $meta);
$synced++;
}
}
}
} elseif (($meta['status'] ?? '') === 'skipped') {
$stats['skipped_images']++;
} elseif (($meta['status'] ?? '') === 'failed') {
$stats['failed_images']++;
}
}
if ($synced > 0) {
$stats['synced_media_library'] = $synced;
}
// Pending = Total - Optimized - Skipped - Failed
$stats['pending_images'] = max(0, $stats['total_images'] - $stats['optimized_images'] - $stats['skipped_images'] - $stats['failed_images']);
return $stats;
}
/**
* Get supported image types.
*/
public function get_supported_types(): array
{
$types = [
'image/jpeg' => ['ext' => 'jpg', 'label' => 'JPEG'],
'image/png' => ['ext' => 'png', 'label' => 'PNG'],
'image/webp' => ['ext' => 'webp', 'label' => 'WebP'],
];
return $types;
}
/**
* Check if image type is supported.
*/
public function is_supported_type(string $mime_type): bool
{
return array_key_exists($mime_type, $this->get_supported_types());
}
/**
* Format bytes to human readable.
*/
public static function format_bytes(int $bytes, int $precision = 2): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
}
// Initialize the extension
add_action('init', function() {
Image_Optimizer::instance();
});