Advertisement
Web_General

PixRefiner 3.4

May 21st, 2025
164
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 108.78 KB | None | 0 0
  1. //PixRefiner v3.4
  2. // Helper function for formatting file sizes
  3. function formatBytes($bytes, $precision = 2) {
  4.     $units = ['B', 'KB', 'MB', 'GB', 'TB'];
  5.     $bytes = max($bytes, 0);
  6.     $pow = ($bytes > 0) ? floor(log($bytes) / log(1024)) : 0;
  7.     $pow = min($pow, count($units) - 1);
  8.     $bytes /= pow(1024, $pow);
  9.     return round($bytes, $precision) . ' ' . $units[$pow];
  10. }
  11.  
  12. // Limit default WordPress sizes to thumbnail only when auto-conversion is enabled
  13. function wpturbo_limit_image_sizes($sizes) {
  14.     if (wpturbo_get_disable_auto_conversion()) {
  15.         return $sizes;
  16.     }
  17.     return isset($sizes['thumbnail']) ? ['thumbnail' => $sizes['thumbnail']] : $sizes;return ['thumbnail' => $sizes['thumbnail']];
  18. }
  19. //Global override for default sizes
  20. add_filter('intermediate_image_sizes_advanced', function($sizes) {
  21.     if (!wpturbo_get_disable_auto_conversion()) {
  22.         return ['thumbnail' => [
  23.             'width' => 150,
  24.             'height' => 150,
  25.             'crop' => true
  26.         ]];
  27.     }
  28.     return $sizes;
  29. }, 99);
  30.  
  31. add_filter('intermediate_image_sizes_advanced', 'wpturbo_limit_image_sizes');
  32.  
  33. // Set thumbnail size to 150x150
  34. function wpturbo_set_thumbnail_size() {
  35.     update_option('thumbnail_size_w', 150);
  36.     update_option('thumbnail_size_h', 150);
  37.     update_option('thumbnail_crop', 1);
  38. }
  39. add_action('admin_init', 'wpturbo_set_thumbnail_size');
  40.  
  41. // Register custom sizes (up to 3 additional sizes beyond the main one)
  42. add_action('after_setup_theme', 'wpturbo_register_custom_sizes');
  43. function wpturbo_register_custom_sizes() {
  44.     $mode = wpturbo_get_resize_mode();
  45.     if ($mode === 'width') {
  46.         $max_values = wpturbo_get_max_widths();
  47.         $additional_values = array_slice($max_values, 1, 3);
  48.         foreach ($additional_values as $width) {
  49.             add_image_size("custom-$width", $width, 0, false);
  50.         }
  51.     } else {
  52.         $max_values = wpturbo_get_max_heights();
  53.         $additional_values = array_slice($max_values, 1, 3);
  54.         foreach ($additional_values as $height) {
  55.             add_image_size("custom-$height", 0, $height, false);
  56.         }
  57.     }
  58. }
  59.  
  60. // Get or set max widths (default to mobile-friendly set, limit to 4)
  61. function wpturbo_get_max_widths() {
  62.     $value = get_option('webp_max_widths', '1920,1200,600,300');
  63.     $widths = array_map('absint', array_filter(explode(',', $value)));
  64.     $widths = array_filter($widths, function($w) { return $w > 0 && $w <= 9999; });
  65.     return array_slice($widths, 0, 4);
  66. }
  67.  
  68. // Get or set max heights (default to mobile-friendly set, limit to 4)
  69. function wpturbo_get_max_heights() {
  70.     $value = get_option('webp_max_heights', '1080,720,480,360');
  71.     $heights = array_map('absint', array_filter(explode(',', $value)));
  72.     $heights = array_filter($heights, function($h) { return $h > 0 && $h <= 9999; });
  73.     return array_slice($heights, 0, 4);
  74. }
  75.  
  76. // Get or set resize mode
  77. function wpturbo_get_resize_mode() {
  78.     return get_option('webp_resize_mode', 'width');
  79. }
  80.  
  81. // Get or set quality (0-100)
  82. function wpturbo_get_quality() {
  83.     return (int) get_option('webp_quality', 80);
  84. }
  85.  
  86. // Get or set batch size
  87. function wpturbo_get_batch_size() {
  88.     return (int) get_option('webp_batch_size', 5);
  89. }
  90.  
  91. // Get or set preserve originals
  92. function wpturbo_get_preserve_originals() {
  93.     return (bool) get_option('webp_preserve_originals', false);
  94. }
  95.  
  96. // Get or set disable auto-conversion on upload
  97. function wpturbo_get_disable_auto_conversion() {
  98.     return (bool) get_option('webp_disable_auto_conversion', false);
  99. }
  100.  
  101. // Get or set minimum size threshold in KB (default to 0, meaning no minimum)
  102. function wpturbo_get_min_size_kb() {
  103.     return (int) get_option('webp_min_size_kb', 0);
  104. }
  105.  
  106. // Get or set whether to use AVIF instead of WebP
  107. function wpturbo_get_use_avif() {
  108.     return (bool) get_option('webp_use_avif', false);
  109. }
  110.  
  111. // Get excluded image IDs
  112. function wpturbo_get_excluded_images() {
  113.     $excluded = get_option('webp_excluded_images', []);
  114.     return is_array($excluded) ? array_map('absint', $excluded) : [];
  115. }
  116.  
  117. // Add an image to the excluded list
  118. function wpturbo_add_excluded_image($attachment_id) {
  119.     $attachment_id = absint($attachment_id);
  120.     $excluded = wpturbo_get_excluded_images();
  121.     if (!in_array($attachment_id, $excluded)) {
  122.         $excluded[] = $attachment_id;
  123.         update_option('webp_excluded_images', array_unique($excluded));
  124.         $log = get_option('webp_conversion_log', []);
  125.         $log[] = sprintf(__('Excluded image added: Attachment ID %d', 'wpturbo'), $attachment_id);
  126.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  127.  
  128.         return true;
  129.     }
  130.     return false;
  131. }
  132.  
  133. // Remove an image from the excluded list
  134. function wpturbo_remove_excluded_image($attachment_id) {
  135.     $attachment_id = absint($attachment_id);
  136.     $excluded = wpturbo_get_excluded_images();
  137.     $index = array_search($attachment_id, $excluded);
  138.     if ($index !== false) {
  139.         unset($excluded[$index]);
  140.         update_option('webp_excluded_images', array_values($excluded));
  141.         $log = get_option('webp_conversion_log', []);
  142.         $log[] = sprintf(__('Excluded image removed: Attachment ID %d', 'wpturbo'), $attachment_id);
  143.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  144.         return true;
  145.     }
  146.     return false;
  147. }
  148.  
  149. // Ensure MIME types are supported in .htaccess (Apache only)
  150. function wpturbo_ensure_mime_types() {
  151.     $htaccess_file = ABSPATH . '.htaccess';
  152.     if (!file_exists($htaccess_file) || !is_writable($htaccess_file)) {
  153.         return false;
  154.     }
  155.  
  156.     $content = file_get_contents($htaccess_file);
  157.     $webp_mime = "AddType image/webp .webp";
  158.     $avif_mime = "AddType image/avif .avif";
  159.  
  160.     if (strpos($content, $webp_mime) === false || strpos($content, $avif_mime) === false) {
  161.         $new_content = "# BEGIN WebP Converter MIME Types\n";
  162.         if (strpos($content, $webp_mime) === false) {
  163.             $new_content .= "$webp_mime\n";
  164.         }
  165.         if (strpos($content, $avif_mime) === false) {
  166.             $new_content .= "$avif_mime\n";
  167.         }
  168.         $new_content .= "# END WebP Converter MIME Types\n";
  169.         $content .= "\n" . $new_content;
  170.         file_put_contents($htaccess_file, $content);
  171.         return true;
  172.     }
  173.     return true;
  174. }
  175.  
  176. // Core conversion function (supports WebP or AVIF)
  177. function wpturbo_convert_to_format($file_path, $dimension, &$log = null, $attachment_id = null, $suffix = '') {
  178.     $use_avif = wpturbo_get_use_avif();
  179.     $format = $use_avif ? 'image/avif' : 'image/webp';
  180.     $extension = $use_avif ? '.avif' : '.webp';
  181.     $path_info = pathinfo($file_path);
  182.     $new_file_path = $path_info['dirname'] . '/' . $path_info['filename'] . $suffix . $extension;
  183.     $quality = wpturbo_get_quality();
  184.     $mode = wpturbo_get_resize_mode();
  185.  
  186.     if (!(extension_loaded('imagick') || extension_loaded('gd'))) {
  187.         if ($log !== null) $log[] = sprintf(__('Error: No image library (Imagick/GD) available for %s', 'wpturbo'), basename($file_path));
  188.         return false;
  189.     }
  190.  
  191.     $has_avif_support = (extension_loaded('imagick') && in_array('AVIF', Imagick::queryFormats())) || (extension_loaded('gd') && function_exists('imageavif'));
  192.     if ($use_avif && !$has_avif_support) {
  193.         if ($log !== null) $log[] = sprintf(__('Error: AVIF not supported on this server for %s', 'wpturbo'), basename($file_path));
  194.         return false;
  195.     }
  196.  
  197.     $editor = wp_get_image_editor($file_path);
  198.     if (is_wp_error($editor)) {
  199.         if ($log !== null) $log[] = sprintf(__('Error: Image editor failed for %s - %s', 'wpturbo'), basename($file_path), $editor->get_error_message());
  200.         return false;
  201.     }
  202.  
  203.     $dimensions = $editor->get_size();
  204.     $resized = false;
  205.     if ($mode === 'width' && $dimensions['width'] > $dimension) {
  206.         $editor->resize($dimension, null, false);
  207.         $resized = true;
  208.     } elseif ($mode === 'height' && $dimensions['height'] > $dimension) {
  209.         $editor->resize(null, $dimension, false);
  210.         $resized = true;
  211.     }
  212.  
  213.     $result = $editor->save($new_file_path, $format, ['quality' => $quality]);
  214.     if (is_wp_error($result)) {
  215.         if ($log !== null) $log[] = sprintf(__('Error: Conversion failed for %s - %s', 'wpturbo'), basename($file_path), $result->get_error_message());
  216.         return false;
  217.     }
  218.  
  219.     if ($log !== null) {
  220.         $log[] = sprintf(
  221.             __('Converted: %s → %s %s', 'wpturbo'),
  222.             basename($file_path),
  223.             basename($new_file_path),
  224.             $resized ? sprintf(__('(resized to %dpx %s, quality %d)', 'wpturbo'), $dimension, $mode, $quality) : sprintf(__('(quality %d)', 'wpturbo'), $quality)
  225.         );
  226.     }
  227.  
  228.     return $new_file_path;
  229. }
  230.  
  231. // Handle new uploads with format conversion
  232. add_filter('wp_handle_upload', 'wpturbo_handle_upload_convert_to_format', 10, 1);
  233. function wpturbo_handle_upload_convert_to_format($upload) {
  234.     if (wpturbo_get_disable_auto_conversion()) {
  235.         return $upload;
  236.     }
  237.  
  238.     $file_extension = strtolower(pathinfo($upload['file'], PATHINFO_EXTENSION));
  239.     $allowed_extensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'];
  240.     if (!in_array($file_extension, $allowed_extensions)) {
  241.         return $upload;
  242.     }
  243.  
  244.     $use_avif = wpturbo_get_use_avif();
  245.     $format = $use_avif ? 'image/avif' : 'image/webp';
  246.     $extension = $use_avif ? '.avif' : '.webp';
  247.  
  248.     $file_path = $upload['file'];
  249.     $uploads_dir = dirname($file_path);
  250.     $log = get_option('webp_conversion_log', []);
  251.  
  252.     // Check if uploads directory is writable
  253.     if (!is_writable($uploads_dir)) {
  254.         $log[] = sprintf(__('Error: Uploads directory %s is not writable', 'wpturbo'), $uploads_dir);
  255.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  256.         return $upload;
  257.     }
  258.  
  259.     $file_size_kb = filesize($file_path) / 1024;
  260.     $min_size_kb = wpturbo_get_min_size_kb();
  261.  
  262.     if ($min_size_kb > 0 && $file_size_kb < $min_size_kb) {
  263.         $log[] = sprintf(__('Skipped: %s (size %s KB < %d KB)', 'wpturbo'), basename($file_path), round($file_size_kb, 2), $min_size_kb);
  264.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  265.         return $upload;
  266.     }
  267.  
  268.     $mode = wpturbo_get_resize_mode();
  269.     $max_values = ($mode === 'width') ? wpturbo_get_max_widths() : wpturbo_get_max_heights();
  270.     $attachment_id = attachment_url_to_postid($upload['url']);
  271.     $new_files = [];
  272.     $success = true;
  273.  
  274.     // Get original image dimensions
  275.     $editor = wp_get_image_editor($file_path);
  276.     if (is_wp_error($editor)) {
  277.         $log[] = sprintf(__('Error: Image editor failed for %s - %s', 'wpturbo'), basename($file_path), $editor->get_error_message());
  278.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  279.         return $upload;
  280.     }
  281.     $dimensions = $editor->get_size();
  282.     $original_width = $dimensions['width'];
  283.  
  284.     // Convert only for sizes smaller than or equal to original width (in width mode)
  285.     $valid_max_values = $max_values;
  286.     if ($mode === 'width') {
  287.         $valid_max_values = array_filter($max_values, function($width, $index) use ($original_width) {
  288.             // Always include the first size (original) or sizes smaller than original width
  289.             return $index === 0 || $width <= $original_width;
  290.         }, ARRAY_FILTER_USE_BOTH);
  291.     }
  292.  
  293.     // Convert all valid sizes and rollback if any fail
  294.     foreach ($valid_max_values as $index => $dimension) {
  295.         $suffix = ($index === 0) ? '' : "-{$dimension}";
  296.         $new_file_path = wpturbo_convert_to_format($file_path, $dimension, $log, $attachment_id, $suffix);
  297.         if ($new_file_path) {
  298.             if ($index === 0) {
  299.                 $upload['file'] = $new_file_path;
  300.                 $upload['url'] = str_replace(basename($file_path), basename($new_file_path), $upload['url']);
  301.                 $upload['type'] = $format;
  302.             }
  303.             $new_files[] = $new_file_path;
  304.         } else {
  305.             $success = false;
  306.             break;
  307.         }
  308.     }
  309.  
  310.     // Generate thumbnail
  311.     if ($success) {
  312.         $editor = wp_get_image_editor($file_path);
  313.         if (!is_wp_error($editor)) {
  314.             $editor->resize(150, 150, true);
  315.             $thumbnail_path = dirname($file_path) . '/' . pathinfo($file_path, PATHINFO_FILENAME) . '-150x150' . $extension;
  316.             $saved = $editor->save($thumbnail_path, $format, ['quality' => wpturbo_get_quality()]);
  317.             if (!is_wp_error($saved)) {
  318.                 $log[] = sprintf(__('Generated thumbnail: %s', 'wpturbo'), basename($thumbnail_path));
  319.                 $new_files[] = $thumbnail_path;
  320.             } else {
  321.                 $success = false;
  322.             }
  323.         } else {
  324.             $success = false;
  325.         }
  326.     }
  327.  
  328.     // Rollback if any conversion failed
  329.     if (!$success) {
  330.         foreach ($new_files as $file) {
  331.             if (file_exists($file)) @unlink($file);
  332.         }
  333.         $log[] = sprintf(__('Error: Conversion failed for %s, rolling back', 'wpturbo'), basename($file_path));
  334.         $log[] = sprintf(__('Original preserved: %s', 'wpturbo'), basename($file_path));
  335.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  336.         return $upload;
  337.     }
  338.  
  339.     // Update metadata only if all conversions succeeded
  340.     if ($attachment_id && !empty($new_files)) {
  341.         $metadata = wp_generate_attachment_metadata($attachment_id, $upload['file']);
  342.         if (!is_wp_error($metadata)) {
  343.             $base_name = pathinfo($file_path, PATHINFO_FILENAME);
  344.             $dirname = dirname($file_path);
  345.             // Add custom sizes
  346.             foreach ($valid_max_values as $index => $dimension) {
  347.                 if ($index === 0) continue;
  348.                 $size_file = "$dirname/$base_name-$dimension$extension";
  349.                 if (file_exists($size_file)) {
  350.                     $metadata['sizes']["custom-$dimension"] = [
  351.                         'file' => "$base_name-$dimension$extension",
  352.                         'width' => ($mode === 'width') ? $dimension : 0,
  353.                         'height' => ($mode === 'height') ? $dimension : 0,
  354.                         'mime-type' => $format
  355.                     ];
  356.                 }
  357.             }
  358.             // Ensure thumbnail metadata is always updated
  359.             $thumbnail_file = "$dirname/$base_name-150x150$extension";
  360.             if (file_exists($thumbnail_file)) {
  361.                 $metadata['sizes']['thumbnail'] = [
  362.                     'file' => "$base_name-150x150$extension",
  363.                     'width' => 150,
  364.                     'height' => 150,
  365.                     'mime-type' => $format
  366.                 ];
  367.             } else {
  368.                 // Regenerate thumbnail if missing
  369.                 $editor = wp_get_image_editor($upload['file']);
  370.                 if (!is_wp_error($editor)) {
  371.                     $editor->resize(150, 150, true);
  372.                     $saved = $editor->save($thumbnail_file, $format, ['quality' => wpturbo_get_quality()]);
  373.                     if (!is_wp_error($saved)) {
  374.                         $metadata['sizes']['thumbnail'] = [
  375.                             'file' => "$base_name-150x150$extension",
  376.                             'width' => 150,
  377.                             'height' => 150,
  378.                             'mime-type' => $format
  379.                         ];
  380.                         $log[] = sprintf(__('Regenerated missing thumbnail: %s', 'wpturbo'), basename($thumbnail_file));
  381.                     }
  382.                 }
  383.             }
  384.             $metadata['webp_quality'] = wpturbo_get_quality();
  385.             $metadata['pixrefiner_stamp'] = [
  386.                 'format' => wpturbo_get_use_avif() ? 'avif' : 'webp',
  387.                 'quality' => wpturbo_get_quality(),
  388.                 'resize_mode' => wpturbo_get_resize_mode(),
  389.                 'max_values' => array_values($valid_max_values), // Use valid sizes in stamp
  390.             ];
  391.             update_attached_file($attachment_id, $upload['file']);
  392.             wp_update_post(['ID' => $attachment_id, 'post_mime_type' => $format]);
  393.             wp_update_attachment_metadata($attachment_id, $metadata);
  394.             if (!empty($metadata['pixrefiner_stamp'])) {
  395.                 $log[] = "Stamp check for Attachment ID {$attachment_id}:";
  396.                 $log[] = "Expected stamp: " . json_encode($metadata['pixrefiner_stamp']);
  397.                 $log[] = "Existing stamp: none (new upload)";
  398.             }
  399.         } else {
  400.             $log[] = sprintf(__('Error: Metadata regeneration failed for %s - %s', 'wpturbo'), basename($file_path), $metadata->get_error_message());
  401.         }
  402.     }
  403.  
  404.     // Delete original only if all conversions succeeded and not preserved
  405.     if ($file_extension !== ($use_avif ? 'avif' : 'webp') && file_exists($file_path) && !wpturbo_get_preserve_originals()) {
  406.         $attempts = 0;
  407.         $chmod_failed = false;
  408.         while ($attempts < 5 && file_exists($file_path)) {
  409.             if (!is_writable($file_path)) {
  410.                 @chmod($file_path, 0644);
  411.                 if (!is_writable($file_path)) {
  412.                     if ($chmod_failed) {
  413.                         $log[] = sprintf(__('Error: Cannot make %s writable after retry - skipping deletion', 'wpturbo'), basename($file_path));
  414.                         break;
  415.                     }
  416.                     $chmod_failed = true;
  417.                 }
  418.             }
  419.             if (@unlink($file_path)) {
  420.                 $log[] = sprintf(__('Deleted original: %s', 'wpturbo'), basename($file_path));
  421.                 break;
  422.             }
  423.             $attempts++;
  424.             sleep(1);
  425.         }
  426.         if (file_exists($file_path)) {
  427.             $log[] = sprintf(__('Error: Failed to delete original %s after 5 retries', 'wpturbo'), basename($file_path));
  428.         }
  429.     }
  430.  
  431.     update_option('webp_conversion_log', array_slice((array)$log, -500));
  432.     return $upload;
  433. }
  434.  
  435. // Fix metadata for converted images
  436. add_filter('wp_generate_attachment_metadata', 'wpturbo_fix_format_metadata', 10, 2);
  437. function wpturbo_fix_format_metadata($metadata, $attachment_id) {
  438.     $use_avif  = wpturbo_get_use_avif();
  439.     $extension = $use_avif ? 'avif' : 'webp';
  440.     $format    = $use_avif ? 'image/avif' : 'image/webp';
  441.  
  442.     $file = get_attached_file($attachment_id);
  443.     if (pathinfo($file, PATHINFO_EXTENSION) !== $extension) {
  444.         return $metadata;
  445.     }
  446.  
  447.     $uploads    = wp_upload_dir();
  448.     $file_path  = $file;
  449.     $file_name  = basename($file_path);
  450.     $dirname    = dirname($file_path);
  451.     $base_name  = pathinfo($file_name, PATHINFO_FILENAME);
  452.     $mode       = wpturbo_get_resize_mode();
  453.     $max_values = ($mode === 'width') ? wpturbo_get_max_widths() : wpturbo_get_max_heights();
  454.  
  455.     $metadata['file']      = str_replace($uploads['basedir'] . '/', '', $file_path);
  456.     $metadata['mime_type'] = $format;
  457.  
  458.     foreach ($max_values as $index => $dimension) {
  459.         if ($index === 0) continue;
  460.         $size_file = "$dirname/$base_name-$dimension.$extension";
  461.         if (file_exists($size_file)) {
  462.             $metadata['sizes']["custom-$dimension"] = [
  463.                 'file'      => "$base_name-$dimension.$extension",
  464.                 'width'     => ($mode === 'width') ? $dimension : 0,
  465.                 'height'    => ($mode === 'height') ? $dimension : 0,
  466.                 'mime-type' => $format
  467.             ];
  468.         }
  469.     }
  470.  
  471.     $thumbnail_file = "$dirname/$base_name-150x150.$extension";
  472.     if (file_exists($thumbnail_file)) {
  473.         $metadata['sizes']['thumbnail'] = [
  474.             'file'      => "$base_name-150x150.$extension",
  475.             'width'     => 150,
  476.             'height'    => 150,
  477.             'mime-type' => $format
  478.         ];
  479.     }
  480.  
  481.     // ✅ Add stamp
  482.     $metadata['pixrefiner_stamp'] = [
  483.         'format'      => $use_avif ? 'avif' : 'webp',
  484.         'quality'     => wpturbo_get_quality(),
  485.         'resize_mode' => $mode,
  486.         'max_values'  => $max_values,
  487.     ];
  488.  
  489.     // ✅ Log stamp
  490.     $log = get_option('webp_conversion_log', []);
  491.     $log[] = "Stamp set via metadata hook for Attachment ID {$attachment_id}:";
  492.     $log[] = "Stamp: " . json_encode($metadata['pixrefiner_stamp']);
  493.     update_option('webp_conversion_log', array_slice((array)$log, -500));
  494.  
  495.     return $metadata;
  496. }
  497.  
  498.  
  499. //
  500. function wpturbo_convert_single_image_checker($attachments) {
  501.     if (empty($attachments) || !is_array($attachments)) {
  502.         return;
  503.     }
  504.  
  505.     $log = [];
  506.  
  507.     foreach ($attachments as $attachment_id) {
  508.         $file_path = get_attached_file($attachment_id);
  509.         $ext = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
  510.         if (!file_exists($file_path) || in_array($ext, ['jpg', 'jpeg', 'png'])) {
  511.             $log[] = sprintf(__('Skipped: Missing or original format (%s) for Attachment ID %d', 'wpturbo'), $ext, $attachment_id);
  512.             continue;
  513.         }
  514.  
  515.         $metadata = wp_get_attachment_metadata($attachment_id);
  516.  
  517.         // Build the expected stamp from current settings
  518.         $expected_stamp = [
  519.             'format'      => wpturbo_get_use_avif() ? 'avif' : 'webp',
  520.             'quality'     => wpturbo_get_quality(),
  521.             'resize_mode' => wpturbo_get_resize_mode(),
  522.             'max_values'  => (wpturbo_get_resize_mode() === 'width') ? wpturbo_get_max_widths() : wpturbo_get_max_heights(),
  523.         ];
  524.  
  525.         $existing_stamp = isset($metadata['pixrefiner_stamp']) ? $metadata['pixrefiner_stamp'] : null;
  526.  
  527.         // 🔍 Stamp comparison log
  528.         $log[] = "Stamp check for Attachment ID {$attachment_id}:";
  529.         $log[] = "Expected stamp: " . json_encode($expected_stamp);
  530.         $log[] = "Existing stamp: " . ($existing_stamp ? json_encode($existing_stamp) : 'none');
  531.  
  532.         if (!empty($existing_stamp) && $existing_stamp !== $expected_stamp && empty($_GET['force_reconvert'])) {
  533.             foreach ($expected_stamp as $key => $value) {
  534.                 if (!isset($existing_stamp[$key]) || $existing_stamp[$key] !== $value) {
  535.                     $log[] = "Mismatch in stamp key '$key': expected " . json_encode($value) . " but got " . json_encode($existing_stamp[$key] ?? null);
  536.                 }
  537.             }
  538.         }
  539.  
  540.         // ✅ If already optimized and no force reconvert
  541.         if (!empty($existing_stamp) && $existing_stamp === $expected_stamp && empty($_GET['force_reconvert'])) {
  542.             $log[] = sprintf(__('Skipped: Already optimized Attachment ID %d', 'wpturbo'), $attachment_id);
  543.             continue;
  544.         }
  545.  
  546.         // 🔥 Proceed with conversion
  547.         $conversion_result = wpturbo_convert_image($file_path, $attachment_id); // <-- your custom image converter
  548.  
  549.         if (!$conversion_result) {
  550.             $log[] = sprintf(__('Failed to convert Attachment ID %d.', 'wpturbo'), $attachment_id);
  551.             continue;
  552.         }
  553.  
  554.         // 🛡️ If successful, update metadata with the new stamp
  555.         if (!empty($metadata)) {
  556.             $metadata['pixrefiner_stamp'] = $expected_stamp;
  557.             wp_update_attachment_metadata($attachment_id, $metadata);
  558.         }
  559.  
  560.         $log[] = sprintf(__('Converted Attachment ID %d successfully.', 'wpturbo'), $attachment_id);
  561.     }
  562.  
  563.     update_option('webp_conversion_log', array_slice((array)$log, -500));
  564.     return $log;
  565. }
  566.  
  567. function wpturbo_convert_single_image() {
  568.     check_ajax_referer('webp_converter_nonce', 'nonce');
  569.     if (!current_user_can('manage_options') || !isset($_POST['offset'])) {
  570.         wp_send_json_error(__('Permission denied or invalid offset', 'wpturbo'));
  571.     }
  572.  
  573.     $offset       = absint($_POST['offset']);
  574.     $batch_size   = wpturbo_get_batch_size();
  575.     $mode         = wpturbo_get_resize_mode();
  576.     $max_values   = ($mode === 'width') ? wpturbo_get_max_widths() : wpturbo_get_max_heights();
  577.     $current_ext  = wpturbo_get_use_avif() ? 'avif' : 'webp';
  578.     $format       = wpturbo_get_use_avif() ? 'image/avif' : 'image/webp';
  579.     $current_qual = wpturbo_get_quality();
  580.  
  581.     wp_raise_memory_limit('image');
  582.     set_time_limit(max(30, 10 * $batch_size));
  583.  
  584.     $attachments = get_posts([
  585.         'post_type'      => 'attachment',
  586.         'post_mime_type' => ['image/jpeg', 'image/png', 'image/webp', 'image/avif'],
  587.         'posts_per_page' => $batch_size,
  588.         'offset'         => $offset,
  589.         'fields'         => 'ids',
  590.         'post__not_in'   => wpturbo_get_excluded_images(),
  591.     ]);
  592.  
  593.     $log = get_option('webp_conversion_log', []);
  594.  
  595.     if (empty($attachments)) {
  596.         update_option('webp_conversion_complete', true);
  597.         $log[] = "<strong style='color:#281E5D;'>" . __('Conversion Complete', 'wpturbo') . '</strong>: ' . __('No more images to process', 'wpturbo');
  598.         update_option('webp_conversion_log', array_slice((array) $log, -500));
  599.         wp_send_json_success(['complete' => true]);
  600.     }
  601.  
  602.     foreach ($attachments as $attachment_id) {
  603.         $file_path = get_attached_file($attachment_id);
  604.         if (!file_exists($file_path)) continue;
  605.  
  606.         // Skip if already optimized and settings match
  607.         $meta = wp_get_attachment_metadata($attachment_id);
  608.         $expected_stamp = [
  609.             'format'      => $current_ext,
  610.             'quality'     => $current_qual,
  611.             'resize_mode' => $mode,
  612.             'max_values'  => $max_values,
  613.         ];
  614.         $existing_stamp = isset($meta['pixrefiner_stamp']) ? $meta['pixrefiner_stamp'] : null;
  615.         if (!empty($existing_stamp) && $existing_stamp === $expected_stamp && empty($_GET['force_reconvert'])) {
  616.             $log[] = sprintf(__('Skipped: Already optimized Attachment ID %d', 'wpturbo'), $attachment_id);
  617.             continue;
  618.         }
  619.  
  620.         $dirname   = dirname($file_path);
  621.         $base_name = pathinfo($file_path, PATHINFO_FILENAME);
  622.         $ext       = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
  623.         $new_files = [];
  624.         $success   = true;
  625.  
  626.         // DELETE OUTDATED or DUPLICATE SIZES FROM FILESYSTEM AND METADATA
  627.         $old_sizes = isset($meta['sizes']) ? $meta['sizes'] : [];
  628.         $main_size = $max_values[0] ?? null;
  629.         $meta['sizes'] = []; // Initialize sizes to empty to clear outdated entries
  630.  
  631.         foreach ($old_sizes as $size_key => $size_data) {
  632.             if (preg_match('/custom-(\d+)/', $size_key, $m)) {
  633.                 $old_dim = (int) $m[1];
  634.                 $is_redundant = (
  635.                     !in_array($old_dim, $max_values)
  636.                     || ($main_size && $old_dim === $main_size)
  637.                 );
  638.                 if ($is_redundant) {
  639.                     $old_file = "$dirname/$base_name-$old_dim.$current_ext";
  640.                     if (file_exists($old_file)) {
  641.                         @unlink($old_file);
  642.                         $log[] = sprintf(__('Deleted duplicate or outdated size: %s', 'wpturbo'), basename($old_file));
  643.                     }
  644.                 } else {
  645.                     // Preserve valid sizes that still exist
  646.                     $meta['sizes'][$size_key] = $size_data;
  647.                 }
  648.             } else if ($size_key === 'thumbnail') {
  649.                 // Always preserve thumbnail if it exists
  650.                 $meta['sizes']['thumbnail'] = $size_data;
  651.             }
  652.         }
  653.  
  654.         // Get original image dimensions
  655.         $editor = wp_get_image_editor($file_path);
  656.         if (is_wp_error($editor)) {
  657.             $log[] = sprintf(__('Error: Image editor failed for %s - %s', 'wpturbo'), basename($file_path), $editor->get_error_message());
  658.             continue;
  659.         }
  660.         $dimensions = $editor->get_size();
  661.         $original_width = $dimensions['width'];
  662.  
  663.         // Filter max_values to include only sizes less than or equal to original width
  664.         $valid_max_values = $max_values;
  665.         if ($mode === 'width') {
  666.             $valid_max_values = array_filter($max_values, function($width, $index) use ($original_width) {
  667.                 return $index === 0 || $width <= $original_width;
  668.             }, ARRAY_FILTER_USE_BOTH);
  669.             // Log skipped sizes for debugging
  670.             if (count($valid_max_values) < count($max_values)) {
  671.                 $skipped = array_diff($max_values, $valid_max_values);
  672.                 $log[] = sprintf(__('Skipped sizes %s for %s (original width %dpx)', 'wpturbo'), implode(', ', $skipped), basename($file_path), $original_width);
  673.             }
  674.         }
  675.  
  676.         // GENERATE NEW SIZES
  677.         foreach ($valid_max_values as $index => $dimension) {
  678.             $suffix = ($index === 0) ? '' : "-$dimension";
  679.             $output = wpturbo_convert_to_format($file_path, $dimension, $log, $attachment_id, $suffix);
  680.             if ($output) {
  681.                 if ($index === 0) {
  682.                     update_attached_file($attachment_id, $output);
  683.                     wp_update_post(['ID' => $attachment_id, 'post_mime_type' => $format]);
  684.                 }
  685.                 $new_files[] = $output;
  686.             } else {
  687.                 $success = false;
  688.                 break;
  689.             }
  690.         }
  691.  
  692.         // GENERATE THUMBNAIL
  693.         $thumb_path = "$dirname/$base_name-150x150.$current_ext";
  694.         if (!file_exists($thumb_path)) {
  695.             $editor = wp_get_image_editor($file_path);
  696.             if (!is_wp_error($editor)) {
  697.                 $editor->resize(150, 150, true);
  698.                 $saved = $editor->save($thumb_path, $format, ['quality' => $current_qual]);
  699.                 if (!is_wp_error($saved)) {
  700.                     $new_files[] = $thumb_path;
  701.                     $log[] = sprintf(__('Generated thumbnail: %s', 'wpturbo'), basename($thumb_path));
  702.                 } else {
  703.                     $success = false;
  704.                 }
  705.             } else {
  706.                 $success = false;
  707.             }
  708.         }
  709.  
  710.         if (!$success) {
  711.             foreach ($new_files as $f) if (file_exists($f)) @unlink($f);
  712.             $log[] = sprintf(__('Error: Conversion failed for %s, rolled back.', 'wpturbo'), basename($file_path));
  713.             continue;
  714.         }
  715.  
  716.         // UPDATE METADATA & STAMP
  717.         if (!empty($new_files)) {
  718.             $meta = wp_generate_attachment_metadata($attachment_id, $new_files[0]);
  719.             if (!is_wp_error($meta)) {
  720.                 $meta['sizes'] = []; // Clear sizes again to ensure only new sizes are included
  721.                 foreach ($valid_max_values as $index => $dimension) {
  722.                     if ($index === 0) continue;
  723.                     $size_file = "$dirname/$base_name-$dimension.$current_ext";
  724.                     if (file_exists($size_file)) {
  725.                         $meta['sizes']["custom-$dimension"] = [
  726.                             'file'      => "$base_name-$dimension.$current_ext",
  727.                             'width'     => ($mode === 'width') ? $dimension : 0,
  728.                             'height'    => ($mode === 'height') ? $dimension : 0,
  729.                             'mime-type' => $format,
  730.                         ];
  731.                     }
  732.                 }
  733.                 if (file_exists($thumb_path)) {
  734.                     $meta['sizes']['thumbnail'] = [
  735.                         'file'      => "$base_name-150x150.$current_ext",
  736.                         'width'     => 150,
  737.                         'height'    => 150,
  738.                         'mime-type' => $format,
  739.                     ];
  740.                 }
  741.                 $meta['webp_quality']      = $current_qual;
  742.                 $meta['pixrefiner_stamp'] = $expected_stamp;
  743.                 wp_update_attachment_metadata($attachment_id, $meta);
  744.             }
  745.         }
  746.  
  747.         // DELETE ORIGINAL IF NOT PRESERVED
  748.         if (!wpturbo_get_preserve_originals() && file_exists($file_path) && $ext !== $current_ext) {
  749.             @unlink($file_path);
  750.             $log[] = sprintf(__('Deleted original: %s', 'wpturbo'), basename($file_path));
  751.         }
  752.  
  753.         $log[] = sprintf(__('Converted Attachment ID %d successfully.', 'wpturbo'), $attachment_id);
  754.     }
  755.  
  756.     update_option('webp_conversion_log', array_slice((array)$log, -500));
  757.     wp_send_json_success([
  758.         'complete' => false,
  759.         'offset'   => $offset + $batch_size
  760.     ]);
  761. }
  762.  
  763. // Progress tracking via AJAX
  764. function wpturbo_webp_conversion_status() {
  765.     check_ajax_referer('webp_converter_nonce', 'nonce');
  766.     if (!current_user_can('manage_options')) {
  767.         wp_send_json_error(__('Permission denied', 'wpturbo'));
  768.     }
  769.  
  770.     $total = wp_count_posts('attachment')->inherit;
  771.     $per_page = 50; // Chunk size for pagination
  772.     $converted = 0;
  773.     $skipped = 0;
  774.  
  775.     // Validate total count
  776.     if ($total < 0) {
  777.         $log = get_option('webp_conversion_log', []);
  778.         $log[] = __('Error: Invalid attachment count detected', 'wpturbo');
  779.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  780.         wp_send_json_error(__('Invalid attachment count', 'wpturbo'));
  781.     }
  782.  
  783.     // Count converted images in chunks
  784.     for ($offset = 0; $offset < $total; $offset += $per_page) {
  785.         $chunk_result = get_posts([
  786.             'post_type' => 'attachment',
  787.             'post_mime_type' => wpturbo_get_use_avif() ? 'image/avif' : 'image/webp',
  788.             'posts_per_page' => $per_page,
  789.             'offset' => $offset,
  790.             'fields' => 'ids',
  791.         ]);
  792.         $converted += count($chunk_result);
  793.     }
  794.  
  795.     // Count skipped images in chunks
  796.     for ($offset = 0; $offset < $total; $offset += $per_page) {
  797.         $chunk_result = get_posts([
  798.             'post_type' => 'attachment',
  799.             'post_mime_type' => ['image/jpeg', 'image/png'],
  800.             'posts_per_page' => $per_page,
  801.             'offset' => $offset,
  802.             'fields' => 'ids',
  803.         ]);
  804.         $skipped += count($chunk_result);
  805.     }
  806.  
  807.     $remaining = $total - $converted - $skipped;
  808.     $excluded_images = wpturbo_get_excluded_images();
  809.     $excluded_data = [];
  810.     foreach ($excluded_images as $id) {
  811.         $thumbnail = wp_get_attachment_image_src($id, 'thumbnail');
  812.         $excluded_data[] = [
  813.             'id' => $id,
  814.             'title' => get_the_title($id),
  815.             'thumbnail' => $thumbnail ? $thumbnail[0] : ''
  816.         ];
  817.     }
  818.  
  819.     $mode = wpturbo_get_resize_mode();
  820.     $max_values = ($mode === 'width') ? wpturbo_get_max_widths() : wpturbo_get_max_heights();
  821.  
  822.     wp_send_json([
  823.         'total' => $total,
  824.         'converted' => $converted,
  825.         'skipped' => $skipped,
  826.         'remaining' => $remaining,
  827.         'excluded' => count($excluded_images),
  828.         'excluded_images' => $excluded_data,
  829.         'log' => get_option('webp_conversion_log', []),
  830.         'complete' => get_option('webp_conversion_complete', false),
  831.         'resize_mode' => $mode,
  832.         'max_values' => implode(', ', $max_values),
  833.         'max_widths' => implode(', ', wpturbo_get_max_widths()),
  834.         'max_heights' => implode(', ', wpturbo_get_max_heights()),
  835.         'quality' => wpturbo_get_quality(),
  836.         'preserve_originals' => wpturbo_get_preserve_originals(),
  837.         'disable_auto_conversion' => wpturbo_get_disable_auto_conversion(),
  838.         'min_size_kb' => wpturbo_get_min_size_kb(),
  839.         'use_avif' => wpturbo_get_use_avif()
  840.     ]);
  841. }
  842.  
  843. // Clear log
  844. function wpturbo_clear_log() {
  845.     if (!isset($_GET['clear_log']) || !current_user_can('manage_options')) {
  846.         return false;
  847.     }
  848.     update_option('webp_conversion_log', [__('Log cleared', 'wpturbo')]);
  849.     return true;
  850. }
  851.  
  852. // Reset to defaults
  853. function wpturbo_reset_defaults() {
  854.     if (!isset($_GET['reset_defaults']) || !current_user_can('manage_options')) {
  855.         return false;
  856.     }
  857.     update_option('webp_max_widths', '1920,1200,600,300');
  858.     update_option('webp_max_heights', '1080,720,480,360');
  859.     update_option('webp_resize_mode', 'width');
  860.     update_option('webp_quality', 80);
  861.     update_option('webp_batch_size', 5);
  862.     update_option('webp_preserve_originals', false);
  863.     update_option('webp_disable_auto_conversion', false);
  864.     update_option('webp_min_size_kb', 0);
  865.     update_option('webp_use_avif', false);
  866.     $log = get_option('webp_conversion_log', []);
  867.     $log[] = __('Settings reset to defaults', 'wpturbo');
  868.     update_option('webp_conversion_log', array_slice((array)$log, -500));
  869.     return true;
  870. }
  871.  
  872. // Set max widths
  873. function wpturbo_set_max_widths() {
  874.     if (!isset($_GET['set_max_width']) || !current_user_can('manage_options') || !isset($_GET['max_width'])) {
  875.         return false;
  876.     }
  877.     $max_widths = sanitize_text_field($_GET['max_width']);
  878.     $width_array = array_filter(array_map('absint', explode(',', $max_widths)));
  879.     $width_array = array_filter($width_array, function($w) { return $w > 0 && $w <= 9999; });
  880.     $width_array = array_slice($width_array, 0, 4);
  881.     if (!empty($width_array)) {
  882.         update_option('webp_max_widths', implode(',', $width_array));
  883.         $log = get_option('webp_conversion_log', []);
  884.         $log[] = sprintf(__('Max widths set to: %spx', 'wpturbo'), implode(', ', $width_array));
  885.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  886.         return true;
  887.     }
  888.     return false;
  889. }
  890.  
  891. // Set max heights
  892. function wpturbo_set_max_heights() {
  893.     if (!isset($_GET['set_max_height']) || !current_user_can('manage_options') || !isset($_GET['max_height'])) {
  894.         return false;
  895.     }
  896.     $max_heights = sanitize_text_field($_GET['max_height']);
  897.     $height_array = array_filter(array_map('absint', explode(',', $max_heights)));
  898.     $height_array = array_filter($height_array, function($h) { return $h > 0 && $h <= 9999; });
  899.     $height_array = array_slice($height_array, 0, 4);
  900.     if (!empty($height_array)) {
  901.         update_option('webp_max_heights', implode(',', $height_array));
  902.         $log = get_option('webp_conversion_log', []);
  903.         $log[] = sprintf(__('Max heights set to: %spx', 'wpturbo'), implode(', ', $height_array));
  904.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  905.         return true;
  906.     }
  907.     return false;
  908. }
  909.  
  910. // Set resize mode
  911. function wpturbo_set_resize_mode() {
  912.     if (!isset($_GET['set_resize_mode']) || !current_user_can('manage_options') || !isset($_GET['resize_mode'])) {
  913.         return false;
  914.     }
  915.     $mode = sanitize_text_field($_GET['resize_mode']);
  916.     if (in_array($mode, ['width', 'height'])) {
  917.         $current_mode = get_option('webp_resize_mode', 'width');
  918.         if ($current_mode !== $mode) {
  919.             update_option('webp_resize_mode', $mode);
  920.             $log = get_option('webp_conversion_log', []);
  921.             $log[] = sprintf(__('Resize mode set to: %s', 'wpturbo'), $mode);
  922.             update_option('webp_conversion_log', array_slice((array)$log, -500));
  923.         }
  924.         return true;
  925.     }
  926.     return false;
  927. }
  928.  
  929. // Set quality
  930. function wpturbo_set_quality() {
  931.     if (!isset($_GET['set_quality']) || !current_user_can('manage_options') || !isset($_GET['quality'])) {
  932.         return false;
  933.     }
  934.     $quality = absint($_GET['quality']);
  935.     if ($quality >= 0 && $quality <= 100) {
  936.         $current_quality = (int) get_option('webp_quality', 80);
  937.         if ($current_quality !== $quality) {
  938.             update_option('webp_quality', $quality);
  939.             $log = get_option('webp_conversion_log', []);
  940.             $log[] = sprintf(__('Quality set to: %d', 'wpturbo'), $quality);
  941.             update_option('webp_conversion_log', array_slice((array)$log, -500));
  942.         }
  943.         return true;
  944.     }
  945.     return false;
  946. }
  947.  
  948. // Set batch size
  949. function wpturbo_set_batch_size() {
  950.     if (!isset($_GET['set_batch_size']) || !current_user_can('manage_options') || !isset($_GET['batch_size'])) {
  951.         return false;
  952.     }
  953.     $batch_size = absint($_GET['batch_size']);
  954.     if ($batch_size > 0 && $batch_size <= 50) {
  955.         update_option('webp_batch_size', $batch_size);
  956.         $log = get_option('webp_conversion_log', []);
  957.         $log[] = sprintf(__('Batch size set to: %d', 'wpturbo'), $batch_size);
  958.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  959.         return true;
  960.     }
  961.     return false;
  962. }
  963.  
  964. // Set preserve originals
  965. function wpturbo_set_preserve_originals() {
  966.     if (!isset($_GET['set_preserve_originals']) || !current_user_can('manage_options') || !isset($_GET['preserve_originals'])) {
  967.         return false;
  968.     }
  969.     $preserve = rest_sanitize_boolean($_GET['preserve_originals']);
  970.     $current_preserve = wpturbo_get_preserve_originals();
  971.     if ($current_preserve !== $preserve) {
  972.         update_option('webp_preserve_originals', $preserve);
  973.         $log = get_option('webp_conversion_log', []);
  974.         $log[] = sprintf(__('Preserve originals set to: %s', 'wpturbo'), $preserve ? __('Yes', 'wpturbo') : __('No', 'wpturbo'));
  975.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  976.         return true;
  977.     }
  978.     return false;
  979. }
  980.  
  981. // Set disable auto-conversion on upload
  982. function wpturbo_set_disable_auto_conversion() {
  983.     if (!isset($_GET['set_disable_auto_conversion']) || !current_user_can('manage_options') || !isset($_GET['disable_auto_conversion'])) {
  984.         return false;
  985.     }
  986.     $disable = rest_sanitize_boolean($_GET['disable_auto_conversion']);
  987.     $current_disable = wpturbo_get_disable_auto_conversion();
  988.     if ($current_disable !== $disable) {
  989.         update_option('webp_disable_auto_conversion', $disable);
  990.         $log = get_option('webp_conversion_log', []);
  991.         $log[] = sprintf(__('Auto-conversion on upload set to: %s', 'wpturbo'), $disable ? __('Disabled', 'wpturbo') : __('Enabled', 'wpturbo'));
  992.        update_option('webp_conversion_log', array_slice((array)$log, -500));
  993.         return true;
  994.     }
  995.     return false;
  996. }
  997.  
  998. // Set minimum size threshold
  999. function wpturbo_set_min_size_kb() {
  1000.     if (!isset($_GET['set_min_size_kb']) || !current_user_can('manage_options') || !isset($_GET['min_size_kb'])) {
  1001.         return false;
  1002.     }
  1003.     $min_size = absint($_GET['min_size_kb']);
  1004.     if ($min_size >= 0) {
  1005.         $current_min_size = wpturbo_get_min_size_kb();
  1006.         if ($current_min_size !== $min_size) {
  1007.             update_option('webp_min_size_kb', $min_size);
  1008.             $log = get_option('webp_conversion_log', []);
  1009.             $log[] = sprintf(__('Minimum size threshold set to: %d KB', 'wpturbo'), $min_size);
  1010.             update_option('webp_conversion_log', array_slice((array)$log, -500));
  1011.         }
  1012.         return true;
  1013.     }
  1014.     return false;
  1015. }
  1016.  
  1017. // Set use AVIF option and ensure MIME types
  1018. function wpturbo_set_use_avif() {
  1019.     if (!isset($_GET['set_use_avif']) || !current_user_can('manage_options') || !isset($_GET['use_avif'])) {
  1020.         return false;
  1021.     }
  1022.     $use_avif = rest_sanitize_boolean($_GET['use_avif']);
  1023.     $current_use_avif = wpturbo_get_use_avif();
  1024.     if ($current_use_avif !== $use_avif) {
  1025.         update_option('webp_use_avif', $use_avif);
  1026.         wpturbo_ensure_mime_types(); // Ensure MIME types are updated
  1027.         $log = get_option('webp_conversion_log', []);
  1028.         $log[] = sprintf(__('Conversion format set to: %s', 'wpturbo'), $use_avif ? 'AVIF' : 'WebP');
  1029.         $log[] = __('Please reconvert all images to ensure consistency after changing formats.', 'wpturbo');
  1030.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  1031.         return true;
  1032.     }
  1033.     return false;
  1034. }
  1035.  
  1036. // Enhanced cleanup of leftover originals and alternate formats
  1037. function wpturbo_cleanup_leftover_originals() {
  1038.     if (!isset($_GET['cleanup_leftover_originals']) || !current_user_can('manage_options')) {
  1039.         return false;
  1040.     }
  1041.  
  1042.     $log = get_option('webp_conversion_log', []);
  1043.     $uploads_dir = wp_upload_dir()['basedir'];
  1044.     $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($uploads_dir));
  1045.     $deleted = 0;
  1046.     $failed = 0;
  1047.     $preserve_originals = wpturbo_get_preserve_originals();
  1048.     $use_avif = wpturbo_get_use_avif();
  1049.     $current_extension = $use_avif ? 'avif' : 'webp';
  1050.     $alternate_extension = $use_avif ? 'webp' : 'avif';
  1051.  
  1052.     $attachments = get_posts([
  1053.         'post_type' => 'attachment',
  1054.         'posts_per_page' => -1,
  1055.         'fields' => 'ids',
  1056.         'post_mime_type' => ['image/jpeg', 'image/png', 'image/webp', 'image/avif'],
  1057.     ]);
  1058.     $active_files = [];
  1059.     $mode = wpturbo_get_resize_mode();
  1060.     $max_values = ($mode === 'width') ? wpturbo_get_max_widths() : wpturbo_get_max_heights();
  1061.     $excluded_images = wpturbo_get_excluded_images();
  1062.  
  1063.     foreach ($attachments as $attachment_id) {
  1064.         $file = get_attached_file($attachment_id);
  1065.         $metadata = wp_get_attachment_metadata($attachment_id);
  1066.         $dirname = dirname($file);
  1067.         $base_name = pathinfo($file, PATHINFO_FILENAME);
  1068.  
  1069.         if (in_array($attachment_id, $excluded_images)) {
  1070.             if ($file && file_exists($file)) $active_files[$file] = true;
  1071.             $possible_extensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'];
  1072.             foreach ($possible_extensions as $ext) {
  1073.                 $potential_file = "$dirname/$base_name.$ext";
  1074.                 if (file_exists($potential_file)) $active_files[$potential_file] = true;
  1075.             }
  1076.             foreach ($max_values as $index => $dimension) {
  1077.                 $suffix = ($index === 0) ? '' : "-{$dimension}";
  1078.                 foreach (['webp', 'avif'] as $ext) {
  1079.                     $file_path = "$dirname/$base_name$suffix.$ext";
  1080.                     if (file_exists($file_path)) $active_files[$file_path] = true;
  1081.                 }
  1082.             }
  1083.             $thumbnail_files = ["$dirname/$base_name-150x150.webp", "$dirname/$base_name-150x150.avif"];
  1084.             foreach ($thumbnail_files as $thumbnail_file) {
  1085.                 if (file_exists($thumbnail_file)) $active_files[$thumbnail_file] = true;
  1086.             }
  1087.             if ($metadata && isset($metadata['sizes'])) {
  1088.                 foreach ($metadata['sizes'] as $size_data) {
  1089.                     $size_file = "$dirname/" . $size_data['file'];
  1090.                     if (file_exists($size_file)) $active_files[$size_file] = true;
  1091.                 }
  1092.             }
  1093.             continue;
  1094.         }
  1095.  
  1096.         if ($file && file_exists($file)) {
  1097.             $active_files[$file] = true;
  1098.             foreach ($max_values as $index => $dimension) {
  1099.                 $suffix = ($index === 0) ? '' : "-{$dimension}";
  1100.                 $current_file = "$dirname/$base_name$suffix.$current_extension";
  1101.                 if (file_exists($current_file)) $active_files[$current_file] = true;
  1102.             }
  1103.             $thumbnail_file = "$dirname/$base_name-150x150.$current_extension";
  1104.             if (file_exists($thumbnail_file)) $active_files[$thumbnail_file] = true;
  1105.         }
  1106.     }
  1107.  
  1108.     if (!$preserve_originals) {
  1109.         foreach ($files as $file) {
  1110.             if ($file->isDir()) continue;
  1111.  
  1112.             $file_path = $file->getPathname();
  1113.             $extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
  1114.             if (!in_array($extension, ['webp', 'avif', 'jpg', 'jpeg', 'png'])) continue;
  1115.  
  1116.             $relative_path = str_replace($uploads_dir . '/', '', $file_path);
  1117.             $path_parts = explode('/', $relative_path);
  1118.             $is_valid_path = (count($path_parts) === 1) || (count($path_parts) === 3 && is_numeric($path_parts[0]) && is_numeric($path_parts[1]));
  1119.  
  1120.             if (!$is_valid_path || isset($active_files[$file_path])) continue;
  1121.  
  1122.             if (in_array($extension, ['jpg', 'jpeg', 'png']) || $extension === $alternate_extension) {
  1123.                 $attempts = 0;
  1124.                 while ($attempts < 5 && file_exists($file_path)) {
  1125.                     if (!is_writable($file_path)) {
  1126.                         @chmod($file_path, 0644);
  1127.                         if (!is_writable($file_path)) {
  1128.                             $log[] = sprintf(__('Error: Cannot make %s writable - skipping deletion', 'wpturbo'), basename($file_path));
  1129.                             $failed++;
  1130.                             break;
  1131.                         }
  1132.                     }
  1133.                     if (@unlink($file_path)) {
  1134.                         $log[] = sprintf(__('Cleanup: Deleted %s', 'wpturbo'), basename($file_path));
  1135.                         $deleted++;
  1136.                         break;
  1137.                     }
  1138.                     $attempts++;
  1139.                     sleep(1);
  1140.                 }
  1141.                 if (file_exists($file_path)) {
  1142.                     $log[] = sprintf(__('Cleanup: Failed to delete %s', 'wpturbo'), basename($file_path));
  1143.                     $failed++;
  1144.                 }
  1145.             }
  1146.         }
  1147.     }
  1148.  
  1149.     $log[] = "<span style='font-weight: bold; color: #281E5D;'>" . __('Cleanup Complete', 'wpturbo') . "</span>: " . sprintf(__('Deleted %d files, %d failed', 'wpturbo'), $deleted, $failed);
  1150.     update_option('webp_conversion_log', array_slice((array)$log, -500));
  1151.  
  1152.     foreach ($attachments as $attachment_id) {
  1153.         if (in_array($attachment_id, $excluded_images)) continue;
  1154.  
  1155.         $file_path = get_attached_file($attachment_id);
  1156.         if (file_exists($file_path) && strtolower(pathinfo($file_path, PATHINFO_EXTENSION)) === $current_extension) {
  1157.             $metadata = wp_get_attachment_metadata($attachment_id);
  1158.             $thumbnail_file = $uploads_dir . '/' . dirname($metadata['file']) . '/' . pathinfo($file_path, PATHINFO_FILENAME) . '-150x150.' . $current_extension;
  1159.             if (!file_exists($thumbnail_file)) {
  1160.                 $metadata = wp_generate_attachment_metadata($attachment_id, $file_path);
  1161.                 if (!is_wp_error($metadata)) {
  1162.                     wp_update_attachment_metadata($attachment_id, $metadata);
  1163.                     $log[] = sprintf(__('Regenerated thumbnail for %s', 'wpturbo'), basename($file_path));
  1164.                 }
  1165.             }
  1166.         }
  1167.     }
  1168.  
  1169.     $log[] = "<span style='font-weight: bold; color: #281E5D;'>" . __('Thumbnail Regeneration Complete', 'wpturbo') . "</span>";
  1170.     update_option('webp_conversion_log', array_slice((array)$log, -500));
  1171.     return true;
  1172. }
  1173.  
  1174. // AJAX handlers for exclusion
  1175. add_action('wp_ajax_webp_add_excluded_image', 'wpturbo_add_excluded_image_ajax');
  1176. function wpturbo_add_excluded_image_ajax() {
  1177.     check_ajax_referer('webp_converter_nonce', 'nonce');
  1178.     if (!current_user_can('manage_options') || !isset($_POST['attachment_id'])) {
  1179.         wp_send_json_error(__('Permission denied or invalid attachment ID', 'wpturbo'));
  1180.     }
  1181.     $attachment_id = absint($_POST['attachment_id']);
  1182.     if (wpturbo_add_excluded_image($attachment_id)) {
  1183.         wp_send_json_success(['message' => __('Image excluded successfully', 'wpturbo')]);
  1184.     } else {
  1185.         wp_send_json_error(__('Image already excluded or invalid ID', 'wpturbo'));
  1186.     }
  1187. }
  1188.  
  1189. add_action('wp_ajax_webp_remove_excluded_image', 'wpturbo_remove_excluded_image_ajax');
  1190. function wpturbo_remove_excluded_image_ajax() {
  1191.     check_ajax_referer('webp_converter_nonce', 'nonce');
  1192.     if (!current_user_can('manage_options') || !isset($_POST['attachment_id'])) {
  1193.         wp_send_json_error(__('Permission denied or invalid attachment ID', 'wpturbo'));
  1194.     }
  1195.     $attachment_id = absint($_POST['attachment_id']);
  1196.     if (wpturbo_remove_excluded_image($attachment_id)) {
  1197.         wp_send_json_success(['message' => __('Image removed from exclusion list', 'wpturbo')]);
  1198.     } else {
  1199.         wp_send_json_error(__('Image not in exclusion list', 'wpturbo'));
  1200.     }
  1201. }
  1202.  
  1203. // Convert post content image URLs to current format
  1204. add_action('wp_ajax_convert_post_images_to_webp', 'wpturbo_convert_post_images_to_format');
  1205. function wpturbo_convert_post_images_to_format() {
  1206.     check_ajax_referer('webp_converter_nonce', 'nonce');
  1207.     if (!current_user_can('manage_options')) {
  1208.         wp_send_json_error(__('Permission denied', 'wpturbo'));
  1209.     }
  1210.  
  1211.     $log = get_option('webp_conversion_log', []);
  1212.     function add_log_entry($message) {
  1213.         global $log;
  1214.         $log[] = "[" . date("Y-m-d H:i:s") . "] " . $message;
  1215.         update_option('webp_conversion_log', array_slice((array)$log, -500));
  1216.     }
  1217.  
  1218.     $use_avif = wpturbo_get_use_avif();
  1219.     $extension = $use_avif ? 'avif' : 'webp';
  1220.     add_log_entry(sprintf(__('Starting post/page/FSE-template image conversion to %s...', 'wpturbo'), $use_avif ? 'AVIF' : 'WebP'));
  1221.  
  1222.     $public_post_types = get_post_types(['public' => true], 'names');
  1223.     $fse_post_types = ['wp_template', 'wp_template_part', 'wp_block'];
  1224.     $post_types = array_unique(array_merge($public_post_types, $fse_post_types));
  1225.  
  1226.     $args = [
  1227.         'post_type' => $post_types,
  1228.         'posts_per_page' => -1,
  1229.         'fields' => 'ids'
  1230.     ];
  1231.     $posts = get_posts($args);
  1232.  
  1233.     if (!$posts) {
  1234.         add_log_entry(__('No posts/pages/FSE-templates found', 'wpturbo'));
  1235.         wp_send_json_success(['message' => __('No posts/pages/FSE-templates found', 'wpturbo')]);
  1236.     }
  1237.  
  1238.     $upload_dir = wp_upload_dir();
  1239.     $upload_baseurl = $upload_dir['baseurl'];
  1240.     $upload_basedir = $upload_dir['basedir'];
  1241.     $updated_count = 0;
  1242.     $checked_images = 0;
  1243.  
  1244.     foreach ($posts as $post_id) {
  1245.         $content = get_post_field('post_content', $post_id);
  1246.         $original_content = $content;
  1247.  
  1248.         $content = preg_replace_callback(
  1249.             '/<img[^>]+src=["\']([^"\']+\.(?:jpg|jpeg|png))["\'][^>]*>/i',
  1250.             function ($matches) use (&$checked_images, $upload_baseurl, $upload_basedir, $extension) {
  1251.                 $original_url = $matches[1];
  1252.                 if (strpos($original_url, $upload_baseurl) !== 0) {
  1253.                     add_log_entry("Skipping <img> (external): {$original_url}");
  1254.                     return $matches[0];
  1255.                 }
  1256.                 $checked_images++;
  1257.  
  1258.                 $dirname = pathinfo($original_url, PATHINFO_DIRNAME);
  1259.                 $filename = pathinfo($original_url, PATHINFO_FILENAME);
  1260.  
  1261.                 $new_url = $dirname . '/' . $filename . '.' . $extension;
  1262.                 $scaled_url = $dirname . '/' . $filename . '-scaled.' . $extension;
  1263.                 $new_path = str_replace($upload_baseurl, $upload_basedir, $new_url);
  1264.                 $scaled_path = str_replace($upload_baseurl, $upload_basedir, $scaled_url);
  1265.  
  1266.                 if (file_exists($scaled_path)) {
  1267.                     add_log_entry(sprintf(__('Replacing: %s → %s', 'wpturbo'), $original_url, $scaled_url));
  1268.                     return str_replace($original_url, $scaled_url, $matches[0]);
  1269.                 }
  1270.                 if (file_exists($new_path)) {
  1271.                     add_log_entry(sprintf(__('Replacing: %s → %s', 'wpturbo'), $original_url, $new_url));
  1272.                     return str_replace($original_url, $new_url, $matches[0]);
  1273.                 }
  1274.  
  1275.                 $base_name = preg_replace('/(-\d+x\d+|-scaled)$/', '', $filename);
  1276.                 $fallback_url = $dirname . '/' . $base_name . '.' . $extension;
  1277.                 $fallback_scaled_url = $dirname . '/' . $base_name . '-scaled.' . $extension;
  1278.                 $fallback_path = str_replace($upload_baseurl, $upload_basedir, $fallback_url);
  1279.                 $fallback_scaled_path = str_replace($upload_baseurl, $upload_basedir, $fallback_scaled_url);
  1280.  
  1281.                 if (file_exists($fallback_scaled_path)) {
  1282.                     add_log_entry(sprintf(__('Replacing: %s → %s', 'wpturbo'), $original_url, $fallback_scaled_url));
  1283.                     return str_replace($original_url, $fallback_scaled_url, $matches[0]);
  1284.                 }
  1285.                 if (file_exists($fallback_path)) {
  1286.                     add_log_entry(sprintf(__('Replacing: %s → %s', 'wpturbo'), $original_url, $fallback_url));
  1287.                     return str_replace($original_url, $fallback_url, $matches[0]);
  1288.                 }
  1289.  
  1290.                 return $matches[0];
  1291.             },
  1292.             $content
  1293.         );
  1294.  
  1295.         $content = preg_replace_callback(
  1296.             '/background-image\s*:\s*url\(\s*[\'"]?([^\'"]+\.(?:jpg|jpeg|png))[\'"]?\s*\)/i',
  1297.             function ($matches) use (&$checked_images, $upload_baseurl, $upload_basedir, $extension) {
  1298.                 $original_url = $matches[1];
  1299.                 if (strpos($original_url, $upload_baseurl) !== 0) {
  1300.                     add_log_entry("Skipping BG (external): {$original_url}");
  1301.                     return $matches[0];
  1302.                 }
  1303.                 $checked_images++;
  1304.  
  1305.                 $dirname = pathinfo($original_url, PATHINFO_DIRNAME);
  1306.                 $filename = pathinfo($original_url, PATHINFO_FILENAME);
  1307.  
  1308.                 $new_url = $dirname . '/' . $filename . '.' . $extension;
  1309.                 $scaled_url = $dirname . '/' . $filename . '-scaled.' . $extension;
  1310.                 $new_path = str_replace($upload_baseurl, $upload_basedir, $new_url);
  1311.                 $scaled_path = str_replace($upload_baseurl, $upload_basedir, $scaled_url);
  1312.  
  1313.                 if (file_exists($scaled_path)) {
  1314.                     add_log_entry(sprintf(__('Replacing: %s → %s', 'wpturbo'), $original_url, $scaled_url));
  1315.                     return str_replace($original_url, $scaled_url, $matches[0]);
  1316.                 }
  1317.                 if (file_exists($new_path)) {
  1318.                     add_log_entry(sprintf(__('Replacing: %s → %s', 'wpturbo'), $original_url, $new_url));
  1319.                     return str_replace($original_url, $new_url, $matches[0]);
  1320.                 }
  1321.  
  1322.                 $base_name = preg_replace('/(-\d+x\d+|-scaled)$/', '', $filename);
  1323.                 $fallback_url = $dirname . '/' . $base_name . '.' . $extension;
  1324.                 $fallback_scaled_url = $dirname . '/' . $base_name . '-scaled.' . $extension;
  1325.                 $fallback_path = str_replace($upload_baseurl, $upload_basedir, $fallback_url);
  1326.                 $fallback_scaled_path = str_replace($upload_baseurl, $upload_basedir, $fallback_scaled_url);
  1327.  
  1328.                 if (file_exists($fallback_scaled_path)) {
  1329.                     add_log_entry(sprintf(__('Replacing: %s → %s', 'wpturbo'), $original_url, $fallback_scaled_url));
  1330.                     return str_replace($original_url, $fallback_scaled_url, $matches[0]);
  1331.                 }
  1332.                 if (file_exists($fallback_path)) {
  1333.                     add_log_entry(sprintf(__('Replacing: %s → %s', 'wpturbo'), $original_url, $fallback_url));
  1334.                     return str_replace($original_url, $fallback_url, $matches[0]);
  1335.                 }
  1336.  
  1337.                 return $matches[0];
  1338.             },
  1339.             $content
  1340.         );
  1341.  
  1342.         if ($content !== $original_content) {
  1343.             wp_update_post(['ID' => $post_id, 'post_content' => $content]);
  1344.             $updated_count++;
  1345.         }
  1346.  
  1347.         $thumbnail_id = get_post_thumbnail_id($post_id);
  1348.         if ($thumbnail_id && !in_array($thumbnail_id, wpturbo_get_excluded_images())) {
  1349.             $thumbnail_path = get_attached_file($thumbnail_id);
  1350.             if ($thumbnail_path && !str_ends_with($thumbnail_path, '.' . $extension)) {
  1351.                 $new_path = preg_replace('/\.(jpg|jpeg|png)$/i', '.' . $extension, $thumbnail_path);
  1352.                 if (file_exists($new_path)) {
  1353.                     update_attached_file($thumbnail_id, $new_path);
  1354.                     wp_update_post(['ID' => $thumbnail_id, 'post_mime_type' => $use_avif ? 'image/avif' : 'image/webp']);
  1355.                     $metadata = wp_generate_attachment_metadata($thumbnail_id, $new_path);
  1356.                     wp_update_attachment_metadata($thumbnail_id, $metadata);
  1357.                     add_log_entry(sprintf(__('Updated thumbnail: %s → %s', 'wpturbo'), basename($thumbnail_path), basename($new_path)));
  1358.                 }
  1359.             }
  1360.         }
  1361.     }
  1362.  
  1363.     add_log_entry(sprintf(__('Checked %d images (including BG), updated %d items', 'wpturbo'), $checked_images, $updated_count));
  1364.     wp_send_json_success(['message' => sprintf(__('Checked %d images (including BG), updated %d items', 'wpturbo'), $checked_images, $updated_count)]);
  1365. }
  1366.  
  1367. // Export all media as a ZIP file
  1368. add_action('wp_ajax_webp_export_media_zip', 'wpturbo_export_media_zip');
  1369. function wpturbo_export_media_zip() {
  1370.     check_ajax_referer('webp_converter_nonce', 'nonce');
  1371.     if (!current_user_can('manage_options')) {
  1372.         wp_send_json_error(__('Permission denied', 'wpturbo'));
  1373.     }
  1374.  
  1375.     wp_raise_memory_limit('admin');
  1376.     set_time_limit(0);
  1377.  
  1378.     $args = [
  1379.         'post_type' => 'attachment',
  1380.         'post_mime_type' => 'image',
  1381.         'posts_per_page' => -1,
  1382.         'fields' => 'ids',
  1383.     ];
  1384.     $attachments = get_posts($args);
  1385.  
  1386.     if (empty($attachments)) {
  1387.         wp_send_json_error(__('No media files found', 'wpturbo'));
  1388.     }
  1389.  
  1390.     $temp_file = tempnam(sys_get_temp_dir(), 'webp_media_export_');
  1391.     if (!$temp_file) {
  1392.         wp_send_json_error(__('Failed to create temporary file', 'wpturbo'));
  1393.     }
  1394.  
  1395.     $zip = new ZipArchive();
  1396.     if ($zip->open($temp_file, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
  1397.         @unlink($temp_file);
  1398.         wp_send_json_error(__('Failed to create ZIP archive', 'wpturbo'));
  1399.     }
  1400.  
  1401.     $upload_dir = wp_upload_dir()['basedir'];
  1402.     $log = get_option('webp_conversion_log', []);
  1403.     $possible_extensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'];
  1404.  
  1405.     foreach ($attachments as $attachment_id) {
  1406.         $file_path = get_attached_file($attachment_id);
  1407.         if (!$file_path || !file_exists($file_path)) {
  1408.             $log[] = sprintf(__('Skipped: Main file not found for Attachment ID %d', 'wpturbo'), $attachment_id);
  1409.             continue;
  1410.         }
  1411.  
  1412.         $dirname = dirname($file_path);
  1413.         $base_name = pathinfo($file_path, PATHINFO_FILENAME);
  1414.         $relative_dir = str_replace($upload_dir . '/', '', $dirname);
  1415.  
  1416.         // Add main file
  1417.         $relative_path = $relative_dir . '/' . basename($file_path);
  1418.         $zip->addFile($file_path, $relative_path);
  1419.         $log[] = sprintf(__('Added to ZIP: %s (Attachment ID %d)', 'wpturbo'), basename($file_path), $attachment_id);
  1420.  
  1421.         // Add metadata sizes
  1422.         $metadata = wp_get_attachment_metadata($attachment_id);
  1423.         if ($metadata && isset($metadata['sizes'])) {
  1424.             foreach ($metadata['sizes'] as $size => $size_data) {
  1425.                 $size_file = $dirname . '/' . $size_data['file'];
  1426.                 if (file_exists($size_file)) {
  1427.                     $relative_size_path = $relative_dir . '/' . $size_data['file'];
  1428.                     $zip->addFile($size_file, $relative_size_path);
  1429.                     $log[] = sprintf(__('Added to ZIP: %s (size: %s, Attachment ID %d)', 'wpturbo'), $size_data['file'], $size, $attachment_id);
  1430.                 }
  1431.             }
  1432.         }
  1433.  
  1434.         // Add all related files with the same base name
  1435.         foreach ($possible_extensions as $ext) {
  1436.             // Check for base file with different extensions (e.g., image.jpg, image.png)
  1437.             $related_file = "$dirname/$base_name.$ext";
  1438.             if (file_exists($related_file) && $related_file !== $file_path) {
  1439.                 $relative_related_path = $relative_dir . '/' . "$base_name.$ext";
  1440.                 $zip->addFile($related_file, $relative_related_path);
  1441.                 $log[] = sprintf(__('Added to ZIP: Related file %s (Attachment ID %d)', 'wpturbo'), "$base_name.$ext", $attachment_id);
  1442.             }
  1443.  
  1444.             // Check for sized versions (e.g., image-500.jpg, image-150x150.png)
  1445.             $pattern = "$dirname/$base_name-*.$ext";
  1446.             $related_files = glob($pattern);
  1447.             foreach ($related_files as $related_file) {
  1448.                 // Skip if already added as main file or metadata size
  1449.                 if ($related_file === $file_path || in_array(basename($related_file), array_column($metadata['sizes'] ?? [], 'file'))) {
  1450.                     continue;
  1451.                 }
  1452.                 $relative_related_path = $relative_dir . '/' . basename($related_file);
  1453.                 $zip->addFile($related_file, $relative_related_path);
  1454.                 $log[] = sprintf(__('Added to ZIP: Related size %s (Attachment ID %d)', 'wpturbo'), basename($related_file), $attachment_id);
  1455.             }
  1456.         }
  1457.     }
  1458.  
  1459.     $zip->close();
  1460.     update_option('webp_conversion_log', array_slice((array)$log, -500));
  1461.  
  1462.     header('Content-Type: application/zip');
  1463.     header('Content-Disposition: attachment; filename="media_export_' . date('Y-m-d_H-i-s') . '.zip"');
  1464.     header('Content-Length: ' . filesize($temp_file));
  1465.  
  1466.     readfile($temp_file);
  1467.     flush();
  1468.  
  1469.     @unlink($temp_file);
  1470.     exit;
  1471. }
  1472.  
  1473. // Custom srcset to include all sizes in current format
  1474. add_filter('wp_calculate_image_srcset', 'wpturbo_custom_srcset', 10, 5);
  1475. function wpturbo_custom_srcset($sources, $size_array, $image_src, $image_meta, $attachment_id) {
  1476.     if (in_array($attachment_id, wpturbo_get_excluded_images())) {
  1477.         return $sources;
  1478.     }
  1479.  
  1480.     $use_avif = wpturbo_get_use_avif();
  1481.     $extension = $use_avif ? '.avif' : '.webp';
  1482.  
  1483.     $mode = wpturbo_get_resize_mode();
  1484.     $max_values = ($mode === 'width') ? wpturbo_get_max_widths() : wpturbo_get_max_heights();
  1485.     $upload_dir = wp_upload_dir();
  1486.     $base_path = $upload_dir['basedir'] . '/' . dirname($image_meta['file']);
  1487.     $base_name = pathinfo($image_meta['file'], PATHINFO_FILENAME);
  1488.     $base_url = $upload_dir['baseurl'] . '/' . dirname($image_meta['file']);
  1489.  
  1490.     foreach ($max_values as $index => $dimension) {
  1491.         if ($index === 0) continue;
  1492.         $file = "$base_path/$base_name-$dimension$extension";
  1493.         if (file_exists($file)) {
  1494.             $size_key = "custom-$dimension";
  1495.             $width = ($mode === 'width') ? $dimension : (isset($image_meta['sizes'][$size_key]['width']) ? $image_meta['sizes'][$size_key]['width'] : 0);
  1496.             $sources[$width] = [
  1497.                 'url' => "$base_url/$base_name-$dimension$extension",
  1498.                 'descriptor' => 'w',
  1499.                 'value' => $width
  1500.             ];
  1501.         }
  1502.     }
  1503.  
  1504.     $thumbnail_file = "$base_path/$base_name-150x150$extension";
  1505.     if (file_exists($thumbnail_file)) {
  1506.         $sources[150] = [
  1507.             'url' => "$base_url/$base_name-150x150$extension",
  1508.             'descriptor' => 'w',
  1509.             'value' => 150
  1510.         ];
  1511.     }
  1512.  
  1513.     return $sources;
  1514. }
  1515.  
  1516.  
  1517. function wpturbo_convert_single_image_stamp_check($attachment_id) {
  1518.     // Your existing settings - you might already have these defined elsewhere
  1519.     $use_avif = false; // or however you detect AVIF
  1520.     $current_quality = 80; // your WebP/AVIF quality setting
  1521.     $mode = 'width'; // resize mode: width, height, or auto
  1522.     $max_values = [1920, 1200, 600, 300]; // your target sizes
  1523.  
  1524.     // 🔥 1. Load the current metadata
  1525.     $metadata = wp_get_attachment_metadata($attachment_id);
  1526.     if (!$metadata) {
  1527.         return; // Can't process without metadata
  1528.     }
  1529.  
  1530.     // 🔥 2. Build the expected stamp
  1531.     $expected_stamp = [
  1532.         'format' => $use_avif ? 'avif' : 'webp',
  1533.         'quality' => $current_quality,
  1534.         'resize_mode' => $mode,
  1535.         'max_values' => $max_values,
  1536.     ];
  1537.  
  1538.     // 🔥 3. Check existing stamp
  1539.     $stamp = isset($metadata['pixrefiner_stamp']) ? $metadata['pixrefiner_stamp'] : null;
  1540.  
  1541.     // 🔥 4. Compare — if matches and no force, skip
  1542.     if ($stamp === $expected_stamp && empty($_GET['force_reconvert'])) {
  1543.         return; // Already converted correctly, no need to reprocess
  1544.     }
  1545.  
  1546.     // 🔥 5. If no match — continue normal conversion
  1547.     // 👉 Now here you do your actual conversion logic like:
  1548.     //     - Resize images
  1549.     //     - Generate WebP / AVIF
  1550.     //     - Compress
  1551.     // etc.
  1552.  
  1553.     // (This part is your existing code that does the heavy work.)
  1554.  
  1555.     // 🔥 6. After conversion, update the stamp
  1556.     $metadata['pixrefiner_stamp'] = $expected_stamp;
  1557.     wp_update_attachment_metadata($attachment_id, $metadata);
  1558. }
  1559.  
  1560.  
  1561. // Admin interface
  1562. add_action('admin_menu', function() {
  1563.     add_media_page(
  1564.         __('PixRefiner', 'wpturbo'),
  1565.         __('PixRefiner', 'wpturbo'),
  1566.         'manage_options',
  1567.         'webp-converter',
  1568.         'wpturbo_webp_converter_page'
  1569.     );
  1570. });
  1571.  
  1572. function wpturbo_webp_converter_page() {
  1573.     wp_enqueue_media();
  1574.     wp_enqueue_script('media-upload');
  1575.     wp_enqueue_style('media');
  1576.  
  1577.     if (isset($_GET['set_max_width'])) wpturbo_set_max_widths();
  1578.     if (isset($_GET['set_max_height'])) wpturbo_set_max_heights();
  1579.     if (isset($_GET['set_resize_mode'])) wpturbo_set_resize_mode();
  1580.     if (isset($_GET['set_quality'])) wpturbo_set_quality();
  1581.     if (isset($_GET['set_batch_size'])) wpturbo_set_batch_size();
  1582.     if (isset($_GET['set_preserve_originals'])) wpturbo_set_preserve_originals();
  1583.     if (isset($_GET['set_disable_auto_conversion'])) wpturbo_set_disable_auto_conversion();
  1584.     if (isset($_GET['set_min_size_kb'])) wpturbo_set_min_size_kb();
  1585.     if (isset($_GET['set_use_avif'])) wpturbo_set_use_avif();
  1586.     if (isset($_GET['cleanup_leftover_originals'])) wpturbo_cleanup_leftover_originals();
  1587.     if (isset($_GET['clear_log'])) wpturbo_clear_log();
  1588.     if (isset($_GET['reset_defaults'])) wpturbo_reset_defaults();
  1589.  
  1590.     $has_image_library = extension_loaded('imagick') || extension_loaded('gd');
  1591.     $has_avif_support = (extension_loaded('imagick') && in_array('AVIF', Imagick::queryFormats())) || (extension_loaded('gd') && function_exists('imageavif'));
  1592.     $htaccess_file = ABSPATH . '.htaccess';
  1593.     // Original check kept as comment for reference:
  1594.     // $mime_configured = file_exists($htaccess_file) && strpos(file_get_contents($htaccess_file), 'AddType image/webp .webp') !== false;
  1595.     $mime_configured = true; // Force to true to suppress the MIME type warning
  1596.     ?>
  1597.     <div class="wrap" style="padding: 0; font-size: 14px;">
  1598.         <div style="display: flex; gap: 10px; align-items: flex-start;">
  1599.             <!-- Column 1: Controls, Excluded Images, How It Works -->
  1600.             <div style="width: 38%; display: flex; flex-direction: column; gap: 10px;">
  1601.                 <!-- Pane 1: Controls -->
  1602.                 <div style="background: #FFFFFF; padding: 20px; border-radius: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
  1603.                     <h1 style="font-size: 20px; font-weight: bold; color: #333; margin: -5px 0 15px 0;"><?php _e('PixRefiner - Image Optimization - v3.4', 'wpturbo'); ?></h1>
  1604.  
  1605.                     <?php if (!$has_image_library): ?>
  1606.                         <div class="notice notice-error" style="margin-bottom: 20px;">
  1607.                             <p><?php _e('Warning: No image processing libraries (Imagick or GD) available. Conversion requires one of these.', 'wpturbo'); ?></p>
  1608.                         </div>
  1609.                     <?php endif; ?>
  1610.                     <?php if (wpturbo_get_use_avif() && !$has_avif_support): ?>
  1611.                         <div class="notice notice-warning" style="margin-bottom: 20px;">
  1612.                             <p><?php _e('Warning: AVIF support is not detected on this server. Enable Imagick with AVIF or GD with AVIF support to use this option.', 'wpturbo'); ?></p>
  1613.                         </div>
  1614.                     <?php endif; ?>
  1615.                     <?php if (!$mime_configured): ?>
  1616.                         <div class="notice notice-warning" style="margin-bottom: 20px;">
  1617.                             <p><?php _e('Warning: WebP/AVIF MIME types may not be configured on your server. Images might not display correctly. Check your server settings (e.g., .htaccess for Apache or MIME types for Nginx).', 'wpturbo'); ?></p>
  1618.                         </div>
  1619.                     <?php endif; ?>
  1620.  
  1621.                     <?php if (current_user_can('manage_options')): ?>
  1622.                         <div style="margin-bottom: 20px;">
  1623.                             <label for="resize-mode" style="font-weight: bold;"><?php _e('Resize Mode:', 'wpturbo'); ?></label><br>
  1624.                             <select id="resize-mode" style="width: 100px; margin-right: 10px; padding: 0px 0px 0px 5px;">
  1625.                                 <option value="width" <?php echo wpturbo_get_resize_mode() === 'width' ? 'selected' : ''; ?>><?php _e('Width', 'wpturbo'); ?></option>
  1626.                                 <option value="height" <?php echo wpturbo_get_resize_mode() === 'height' ? 'selected' : ''; ?>><?php _e('Height', 'wpturbo'); ?></option>
  1627.                             </select>
  1628.                         </div>
  1629.                         <div style="margin-bottom: 20px;">
  1630.                             <label for="max-width-input" style="font-weight: bold;"><?php _e('Max Widths (up to 4, e.g., 1920, 1200, 600, 300) - 150 is set automatically:', 'wpturbo'); ?></label><br>
  1631.                             <input type="text" id="max-width-input" value="<?php echo esc_attr(implode(', ', wpturbo_get_max_widths())); ?>" style="width: 200px; margin-right: 10px; padding: 5px;" placeholder="1920,1200,600,300">
  1632.                             <button id="set-max-width" class="button"><?php _e('Set Widths', 'wpturbo'); ?></button>
  1633.                         </div>
  1634.                         <div style="margin-bottom: 20px;">
  1635.                             <label for="max-height-input" style="font-weight: bold;"><?php _e('Max Heights (up to 4, e.g., 1080, 720, 480, 360) - 150 is set automatically:', 'wpturbo'); ?></label><br>
  1636.                             <input type="text" id="max-height-input" value="<?php echo esc_attr(implode(', ', wpturbo_get_max_heights())); ?>" style="width: 200px; margin-right: 10px; padding: 5px;" placeholder="1080,720,480,360">
  1637.                             <button id="set-max-height" class="button"><?php _e('Set Heights', 'wpturbo'); ?></button>
  1638.                         </div>
  1639.                         <div style="margin-bottom: 20px;">
  1640.                             <label for="min-size-kb" style="font-weight: bold;"><?php _e('Min Size for Conversion (KB, Set to 0 to disable):', 'wpturbo'); ?></label><br>
  1641.                             <input type="number" id="min-size-kb" value="<?php echo esc_attr(wpturbo_get_min_size_kb()); ?>" min="0" style="width: 50px; margin-right: 10px; padding: 5px;" placeholder="0">
  1642.                             <button id="set-min-size-kb" class="button"><?php _e('Set Min Size', 'wpturbo'); ?></button>
  1643.                         </div>
  1644.                         <div style="margin-bottom: 20px;">
  1645.                             <label><input type="checkbox" id="use-avif" <?php echo wpturbo_get_use_avif() ? 'checked' : ''; ?>> <?php _e('Set to AVIF Conversion (not WebP)', 'wpturbo'); ?></label>
  1646.                         </div>
  1647.                         <div style="margin-bottom: 20px;">
  1648.                             <label><input type="checkbox" id="preserve-originals" <?php echo wpturbo_get_preserve_originals() ? 'checked' : ''; ?>> <?php _e('Preserve Original Files', 'wpturbo'); ?></label>
  1649.                         </div>
  1650.                         <div style="margin-bottom: 20px;">
  1651.                             <label><input type="checkbox" id="disable-auto-conversion" <?php echo wpturbo_get_disable_auto_conversion() ? 'checked' : ''; ?>> <?php _e('Disable Auto-Conversion on Upload', 'wpturbo'); ?></label>
  1652.                         </div>
  1653.                         <div style="margin-bottom: 20px; display: flex; gap: 10px;">
  1654.                             <button id="start-conversion" class="button"><?php _e('1. Convert/Scale', 'wpturbo'); ?></button>
  1655.                             <button id="cleanup-originals" class="button"><?php _e('2. Cleanup Images', 'wpturbo'); ?></button>
  1656.                             <button id="convert-post-images" class="button"><?php _e('3. Fix URLs', 'wpturbo'); ?></button>
  1657.                             <button id="run-all" class="button button-primary"><?php _e('Run All (1-3)', 'wpturbo'); ?></button>
  1658.                             <button id="stop-conversion" class="button" style="display: none;"><?php _e('Stop', 'wpturbo'); ?></button>
  1659.                         </div>
  1660.                         <div style="margin-bottom: 20px; display: flex; gap: 10px;">
  1661.                             <button id="clear-log" class="button"><?php _e('Clear Log', 'wpturbo'); ?></button>
  1662.                             <button id="reset-defaults" class="button"><?php _e('Reset Defaults', 'wpturbo'); ?></button>
  1663.                             <button id="export-media-zip" class="button"><?php _e('Export Media as ZIP', 'wpturbo'); ?></button>
  1664.                         </div>
  1665.                     <?php else: ?>
  1666.                         <p><?php _e('You need manage_options permission to use this tool.', 'wpturbo'); ?></p>
  1667.                     <?php endif; ?>
  1668.                 </div>
  1669.  
  1670.                                 <!-- Pane 2: Exclude Images -->
  1671.                 <div style="background: #FFFFFF; padding: 20px; border-radius: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
  1672.                     <h2 style="font-size: 16px; margin: 0 0 15px 0;"><?php _e('Exclude Images', 'wpturbo'); ?></h2>
  1673.                     <button id="open-media-library" class="button" style="margin-bottom: 20px;"><?php _e('Add from Media Library', 'wpturbo'); ?></button>
  1674.                     <div id="excluded-images">
  1675.                         <h3 style="font-size: 14px; margin: 0 0 10px 0;"><?php _e('Excluded Images', 'wpturbo'); ?></h3>
  1676.                         <ul id="excluded-images-list" style="list-style: none; padding: 0; max-height: 300px; overflow-y: auto;"></ul>
  1677.                     </div>
  1678.                 </div>
  1679.  
  1680.                 <!-- Pane 3: How It Works -->
  1681.                 <div style="background: #FFFFFF; padding: 20px; border-radius: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
  1682.                     <h2 style="font-size: 16px; margin: 0 0 15px 0;"><?php _e('How It Works', 'wpturbo'); ?></h2>
  1683.                     <p style="line-height: 1.5;">
  1684.                         <?php _e('Refine images to WebP or AVIF, and remove excess files to save space.', 'wpturbo'); ?><br><br>
  1685.                         <b><?php _e('Set Auto-Conversion for New Uploads:', 'wpturbo'); ?></b><br>
  1686.                         <b>1. Resize Mode:</b> <?php _e('Pick if images shrink by width or height.', 'wpturbo'); ?><br>
  1687.                         <b>2. Set Max Sizes:</b> <?php _e('Choose up to 4 sizes (150x150 thumbnail is automatic).', 'wpturbo'); ?><br>
  1688.                         <b>3. Min Size for Conversion:</b> <?php _e('Sizes below the min are not affected. Default is 0.', 'wpturbo'); ?><br>
  1689.                         <b>4. Conversion Format:</b> <?php _e('Check to use AVIF. WebP is default.', 'wpturbo'); ?><br>
  1690.                         <b>5. Preserve Originals:</b> <?php _e('Check to stop original files from converting/deleting.', 'wpturbo'); ?><br>
  1691.                         <b>6. Disable Auto-Conversion:</b> <?php _e('Images will convert on upload unless this is ticked.', 'wpturbo'); ?><br>
  1692.                         <b>7. Upload:</b> <?php _e('Upload to Media Library or via elements/widgets.', 'wpturbo'); ?><br><br>
  1693.                         <b><?php _e('Apply for Existing Images:', 'wpturbo'); ?></b><br>
  1694.                         <b>1. Repeat:</b> <?php _e('Set up steps 1-6 above.', 'wpturbo'); ?><br>
  1695.                         <b>2. Run All:</b> <?php _e('Hit "Run All" to do everything at once.', 'wpturbo'); ?><br><br>
  1696.                         <b><?php _e('Apply Manually for Existing Images:', 'wpturbo'); ?></b><br>
  1697.                         <b>1. Repeat:</b> <?php _e('Set up steps 1-6 above.', 'wpturbo'); ?><br>
  1698.                         <b>2. Convert:</b> <?php _e('Change image sizes and format.', 'wpturbo'); ?><br>
  1699.                         <b>3. Cleanup:</b> <?php _e('Delete old formats/sizes (if not preserved).', 'wpturbo'); ?><br>
  1700.                         <b>4. Fix Links:</b> <?php _e('Update image links to the new format.', 'wpturbo'); ?><br><br>
  1701.                         <b><?php _e('IMPORTANT:', 'wpturbo'); ?></b><br>
  1702.                         <b>a) Usability:</b> <?php _e('This tool is ideal for New Sites. Using with Legacy Sites must be done with care as variation due to methods, systems, sizes, can affect the outcome. Please use this tool carefully and at your own risk, as I cannot be held responsible for any issues that may arise from its use.', 'wpturbo'); ?><br>
  1703.                         <b>b) Backups:</b> <?php _e('Use a strong backup tool like All-in-One WP Migration before using this tool. Check if your host saves backups - as some charge a fee to restore.', 'wpturbo'); ?><br>
  1704.                         <b>c) Export Media:</b> <?php _e('Export images as a Zipped Folder prior to running.', 'wpturbo'); ?><br>
  1705.                         <b>d) Reset Defaults:</b> <?php _e('Resets all Settings 1-6.', 'wpturbo'); ?><br>
  1706.                         <b>e) Speed:</b> <?php _e('Bigger sites take longer to run. This depends on your server.', 'wpturbo'); ?><br>
  1707.                         <b>f) Log Wait:</b> <?php _e('Updates show every 50 images.', 'wpturbo'); ?><br>
  1708.                         <b>g) Stop Anytime:</b> <?php _e('Click "Stop" to pause.', 'wpturbo'); ?><br>
  1709.                         <b>h) AVIF Needs:</b> <?php _e('Your server must support AVIF. Check logs if it fails.', 'wpturbo'); ?><br>
  1710.                         <b>i) Old Browsers:</b> <?php _e('AVIF might not work on older browsers, WebP is safer.', 'wpturbo'); ?><br>
  1711.                         <b>j) MIME Types:</b> <?php _e('Server must support WebP/AVIF MIME (check with host).', 'wpturbo'); ?><br>
  1712.                         <b>k) Rollback:</b> <?php _e('If conversion fails, then rollback occurs, and prevents deletion of the original, regardless of whether the Preserve Originals is checked or not.', 'wpturbo'); ?>
  1713.                     </p>
  1714.                     <!-- Donate Button -->
  1715.                     <div style="margin-top: 20px; display: flex; justify-content: flex-start;">
  1716.                         <a href="https://www.paypal.com/paypalme/iamimransiddiq" target="_blank" class="button" style="border: none;" rel="noopener"><?php _e('Support Imran', 'wpturbo'); ?></a>
  1717.                     </div>
  1718.                 </div>
  1719.             </div>
  1720.  
  1721.             <!-- Column 2: Log -->
  1722.             <div style="width: 62%; min-height: 100vh; background: #FFFFFF; padding: 20px; border-radius: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; flex-direction: column;">
  1723.                 <h3 style="font-size: 16px; margin: 0 0 10px 0;"><?php _e('Log (Last 500 Entries)', 'wpturbo'); ?></h3>
  1724.                 <pre id="log" style="background: #f9f9f9; padding: 15px; flex: 1; overflow-y: auto; border: 1px solid #ddd; border-radius: 5px; font-size: 13px;"></pre>
  1725.             </div>
  1726.         </div>
  1727.     </div>
  1728.  
  1729.     <style>
  1730.     #quality-slider {
  1731.         -webkit-appearance: none;
  1732.         height: 6px;
  1733.         border-radius: 3px;
  1734.         background: #ddd;
  1735.     }
  1736.     #quality-slider::-webkit-slider-thumb {
  1737.         -webkit-appearance: none;
  1738.         width: 16px;
  1739.         height: 16px;
  1740.         background: var(--primary-color);
  1741.         border-radius: 50%;
  1742.         cursor: pointer;
  1743.     }
  1744.     .button.button-primary {
  1745.         background: #FF0050;
  1746.         color: #fff;
  1747.         padding: 2px 10px;
  1748.         height: 30px;
  1749.         line-height: 26px;
  1750.         transition: all 0.2s;
  1751.         font-size: 14px;
  1752.         font-weight: 600;
  1753.         border: none;
  1754.     }
  1755.     .button.button-primary:hover {
  1756.         background: #444444;
  1757.     }
  1758.     .button:not(.button-primary) {
  1759.         background: #dbe2e9;
  1760.         color: #444444;
  1761.         padding: 2px 10px;
  1762.         height: 30px;
  1763.         line-height: 26px;
  1764.         transition: all 0.2s;
  1765.         border: none;
  1766.     }
  1767.     .button:not(.button-primary):hover {
  1768.         background: #444444;
  1769.         color: #FFF;
  1770.     }
  1771.     #excluded-images-list li {
  1772.         display: flex;
  1773.         align-items: center;
  1774.         margin-bottom: 10px;
  1775.     }
  1776.     #excluded-images-list img {
  1777.         max-width: 50px;
  1778.         margin-right: 10px;
  1779.     }
  1780.     input[type="text"],
  1781.     input[type="number"],
  1782.     select {
  1783.         padding: 2px;
  1784.         height: 30px;
  1785.         box-sizing: border-box;
  1786.     }
  1787.     @media screen and (max-width: 782px) {
  1788.         div[style*="width: 55%"] {
  1789.             height: calc(100vh - 46px) !important;
  1790.         }
  1791.     }
  1792.     </style>
  1793.  
  1794.     <script>
  1795.         document.addEventListener('DOMContentLoaded', function() {
  1796.             let isConverting = false;
  1797.  
  1798.             function updateStatus() {
  1799.                 fetch('<?php echo admin_url('admin-ajax.php?action=webp_status&nonce=' . wp_create_nonce('webp_converter_nonce')); ?>')
  1800.                     .then(response => {
  1801.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  1802.                         return response.json();
  1803.                     })
  1804.                     .then(data => {
  1805.                         document.getElementById('log').innerHTML = data.log.reverse().join('<br>');
  1806.                         document.getElementById('resize-mode').value = data.resize_mode;
  1807.                         document.getElementById('max-width-input').value = data.max_widths;
  1808.                         document.getElementById('max-height-input').value = data.max_heights;
  1809.                         document.getElementById('preserve-originals').checked = data.preserve_originals;
  1810.                         document.getElementById('disable-auto-conversion').checked = data.disable_auto_conversion;
  1811.                         document.getElementById('min-size-kb').value = data.min_size_kb;
  1812.                         document.getElementById('use-avif').checked = data.use_avif;
  1813.                         updateExcludedImages(data.excluded_images);
  1814.                     })
  1815.                     .catch(error => {
  1816.                         console.error('Error in updateStatus:', error);
  1817.                         alert('Failed to update status: ' + error.message);
  1818.                     });
  1819.             }
  1820.  
  1821.             function updateExcludedImages(excludedImages) {
  1822.                 const list = document.getElementById('excluded-images-list');
  1823.                 list.innerHTML = '';
  1824.                 excludedImages.forEach(image => {
  1825.                     const li = document.createElement('li');
  1826.                     li.innerHTML = `<img decoding="async" src="${image.thumbnail}" alt="${image.title}"><span>${image.title} (ID: ${image.id})</span><button class="remove-excluded button" data-id="${image.id}"><?php echo esc_html__('Remove', 'wpturbo'); ?></button>`;
  1827.                     list.appendChild(li);
  1828.                 });
  1829.                 document.querySelectorAll('.remove-excluded').forEach(button => {
  1830.                     button.addEventListener('click', () => {
  1831.                         fetch('<?php echo admin_url('admin-ajax.php?action=webp_remove_excluded_image&nonce=' . wp_create_nonce('webp_converter_nonce')); ?>', {
  1832.                             method: 'POST',
  1833.                             headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  1834.                             body: 'attachment_id=' + encodeURIComponent(button.getAttribute('data-id'))
  1835.                         })
  1836.                         .then(response => response.json())
  1837.                         .then(data => {
  1838.                             if (data.success) updateStatus();
  1839.                             else alert('Error: ' + data.data);
  1840.                         })
  1841.                         .catch(error => {
  1842.                             console.error('Error removing excluded image:', error);
  1843.                             alert('Failed to remove excluded image: ' + error.message);
  1844.                         });
  1845.                     });
  1846.                 });
  1847.             }
  1848.  
  1849.             let retryCounts = {};
  1850.  
  1851.             function convertNextImage(offset) {
  1852.                 if (!isConverting) return;
  1853.                 retryCounts = {}; // Clear retry counts when starting
  1854.  
  1855.                 fetch('<?php echo admin_url('admin-ajax.php?action=webp_convert_single&nonce=' . wp_create_nonce('webp_converter_nonce')); ?>', {
  1856.                     method: 'POST',
  1857.                     headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  1858.                     body: 'offset=' + encodeURIComponent(offset)
  1859.                 })
  1860.                 .then(response => {
  1861.                     if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  1862.                     return response.json();
  1863.                 })
  1864.                 .then(data => {
  1865.                     if (data.success) {
  1866.                         updateStatus();
  1867.                         if (!data.data.complete && isConverting) {
  1868.                             retryCounts[offset] = 0; // Reset retry count
  1869.                             convertNextImage(data.data.offset);
  1870.                         } else {
  1871.                             document.getElementById('stop-conversion').style.display = 'none';
  1872.                         }
  1873.                     } else {
  1874.                         // Failed: Retry up to 2 times
  1875.                         retryCounts[offset] = (retryCounts[offset] || 0) + 1;
  1876.                         if (retryCounts[offset] <= 2) {
  1877.                             console.warn('Retrying offset:', offset, 'Attempt:', retryCounts[offset]);
  1878.                             setTimeout(() => convertNextImage(offset), 1000); // Wait 1s before retry
  1879.                         } else {
  1880.                             console.error('Giving up on offset:', offset);
  1881.                             if (isConverting) {
  1882.                                 convertNextImage(offset + <?php echo wpturbo_get_batch_size(); ?>); // Skip to next batch
  1883.                             }
  1884.                         }
  1885.                     }
  1886.                 })
  1887.                 .catch(error => {
  1888.                     console.error('Error in convertNextImage:', error);
  1889.                     alert('Conversion failed: ' + error.message);
  1890.                     document.getElementById('stop-conversion').style.display = 'none';
  1891.                 });
  1892.             }
  1893.  
  1894.             <?php if (current_user_can('manage_options')): ?>
  1895.             const mediaFrame = wp.media({
  1896.                 title: '<?php echo esc_js(__('Select Images to Exclude', 'wpturbo')); ?>',
  1897.                 button: { text: '<?php echo esc_js(__('Add to Excluded List', 'wpturbo')); ?>' },
  1898.                 multiple: true,
  1899.                 library: { type: 'image' }
  1900.             });
  1901.             document.getElementById('open-media-library').addEventListener('click', () => mediaFrame.open());
  1902.             mediaFrame.on('select', () => {
  1903.                 const selection = mediaFrame.state().get('selection');
  1904.                 selection.each(attachment => {
  1905.                     fetch('<?php echo admin_url('admin-ajax.php?action=webp_add_excluded_image&nonce=' . wp_create_nonce('webp_converter_nonce')); ?>', {
  1906.                         method: 'POST',
  1907.                         headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  1908.                         body: 'attachment_id=' + encodeURIComponent(attachment.id)
  1909.                     })
  1910.                     .then(response => response.json())
  1911.                     .then(data => {
  1912.                         if (data.success) updateStatus();
  1913.                     })
  1914.                     .catch(error => {
  1915.                         console.error('Error adding excluded image:', error);
  1916.                         alert('Failed to add excluded image: ' + error.message);
  1917.                     });
  1918.                 });
  1919.             });
  1920.  
  1921.             document.getElementById('set-max-width').addEventListener('click', () => {
  1922.                 const maxWidths = document.getElementById('max-width-input').value;
  1923.                 fetch('<?php echo admin_url('admin.php?page=webp-converter&set_max_width=1&max_width='); ?>' + encodeURIComponent(maxWidths))
  1924.                     .then(response => {
  1925.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  1926.                         return response;
  1927.                     })
  1928.                     .then(() => updateStatus())
  1929.                     .catch(error => {
  1930.                         console.error('Error setting max width:', error);
  1931.                         alert('Failed to set max width: ' + error.message);
  1932.                     });
  1933.             });
  1934.  
  1935.             document.getElementById('set-max-height').addEventListener('click', () => {
  1936.                 const maxHeights = document.getElementById('max-height-input').value;
  1937.                 fetch('<?php echo admin_url('admin.php?page=webp-converter&set_max_height=1&max_height='); ?>' + encodeURIComponent(maxHeights))
  1938.                     .then(response => {
  1939.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  1940.                         return response;
  1941.                     })
  1942.                     .then(() => updateStatus())
  1943.                     .catch(error => {
  1944.                         console.error('Error setting max height:', error);
  1945.                         alert('Failed to set max height: ' + error.message);
  1946.                     });
  1947.             });
  1948.  
  1949.             document.getElementById('resize-mode').addEventListener('change', () => {
  1950.                 const mode = document.getElementById('resize-mode').value;
  1951.                 fetch('<?php echo admin_url('admin.php?page=webp-converter&set_resize_mode=1&resize_mode='); ?>' + encodeURIComponent(mode))
  1952.                     .then(response => {
  1953.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  1954.                         return response;
  1955.                     })
  1956.                     .then(() => updateStatus())
  1957.                     .catch(error => {
  1958.                         console.error('Error setting resize mode:', error);
  1959.                         alert('Failed to set resize mode: ' + error.message);
  1960.                     });
  1961.             });
  1962.  
  1963.             document.getElementById('preserve-originals').addEventListener('click', () => {
  1964.                 const preserve = document.getElementById('preserve-originals').checked;
  1965.                 fetch('<?php echo admin_url('admin.php?page=webp-converter&set_preserve_originals=1&preserve_originals='); ?>' + encodeURIComponent(preserve ? 1 : 0))
  1966.                     .then(response => {
  1967.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  1968.                         return response;
  1969.                     })
  1970.                     .then(() => updateStatus())
  1971.                     .catch(error => {
  1972.                         console.error('Error setting preserve originals:', error);
  1973.                         alert('Failed to set preserve originals: ' + error.message);
  1974.                     });
  1975.             });
  1976.  
  1977.             document.getElementById('disable-auto-conversion').addEventListener('click', () => {
  1978.                 const disable = document.getElementById('disable-auto-conversion').checked;
  1979.                 fetch('<?php echo admin_url('admin.php?page=webp-converter&set_disable_auto_conversion=1&disable_auto_conversion='); ?>' + encodeURIComponent(disable ? 1 : 0))
  1980.                     .then(response => {
  1981.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  1982.                         return response;
  1983.                     })
  1984.                     .then(() => updateStatus())
  1985.                     .catch(error => {
  1986.                         console.error('Error setting disable auto-conversion:', error);
  1987.                         alert('Failed to set disable auto-conversion: ' + error.message);
  1988.                     });
  1989.             });
  1990.  
  1991.             document.getElementById('set-min-size-kb').addEventListener('click', () => {
  1992.                 const minSizeKB = document.getElementById('min-size-kb').value;
  1993.                 fetch('<?php echo admin_url('admin.php?page=webp-converter&set_min_size_kb=1&min_size_kb='); ?>' + encodeURIComponent(minSizeKB))
  1994.                     .then(response => {
  1995.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  1996.                         return response;
  1997.                     })
  1998.                     .then(() => updateStatus())
  1999.                     .catch(error => {
  2000.                         console.error('Error setting minimum size:', error);
  2001.                         alert('Failed to set minimum size: ' + error.message);
  2002.                     });
  2003.             });
  2004.  
  2005.             document.getElementById('use-avif').addEventListener('click', () => {
  2006.                 const useAvif = document.getElementById('use-avif').checked;
  2007.                 if (useAvif && !confirm('<?php echo esc_js(__('Switching to AVIF requires reconverting all images for consistency. Continue?', 'wpturbo')); ?>')) {
  2008.                     document.getElementById('use-avif').checked = false;
  2009.                     return;
  2010.                 }
  2011.                 fetch('<?php echo admin_url('admin.php?page=webp-converter&set_use_avif=1&use_avif='); ?>' + encodeURIComponent(useAvif ? 1 : 0))
  2012.                     .then(response => {
  2013.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  2014.                         return response;
  2015.                     })
  2016.                     .then(() => updateStatus())
  2017.                     .catch(error => {
  2018.                         console.error('Error setting AVIF option:', error);
  2019.                         alert('Failed to set AVIF option: ' + error.message);
  2020.                     });
  2021.             });
  2022.  
  2023.             document.getElementById('start-conversion').addEventListener('click', () => {
  2024.                 isConverting = true;
  2025.                 document.getElementById('stop-conversion').style.display = 'inline-block';
  2026.                 fetch('<?php echo admin_url('admin.php?page=webp-converter&convert_existing_images_to_webp=1'); ?>')
  2027.                     .then(response => {
  2028.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  2029.                         return response;
  2030.                     })
  2031.                     .then(() => {
  2032.                         updateStatus();
  2033.                         convertNextImage(0);
  2034.                     })
  2035.                     .catch(error => {
  2036.                         console.error('Error starting conversion:', error);
  2037.                         alert('Failed to start conversion: ' + error.message);
  2038.                     });
  2039.             });
  2040.  
  2041.             document.getElementById('cleanup-originals').addEventListener('click', () => {
  2042.                 fetch('<?php echo admin_url('admin.php?page=webp-converter&cleanup_leftover_originals=1'); ?>')
  2043.                     .then(response => {
  2044.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  2045.                         return response;
  2046.                     })
  2047.                     .then(() => updateStatus())
  2048.                     .catch(error => {
  2049.                         console.error('Error cleaning up originals:', error);
  2050.                         alert('Failed to cleanup originals: ' + error.message);
  2051.                     });
  2052.             });
  2053.  
  2054.             document.getElementById('convert-post-images').addEventListener('click', () => {
  2055.                 if (confirm('<?php echo esc_js(__('Update all post images to the selected format?', 'wpturbo')); ?>')) {
  2056.                     fetch('<?php echo admin_url('admin-ajax.php?action=convert_post_images_to_webp&nonce=' . wp_create_nonce('webp_converter_nonce')); ?>', {
  2057.                         method: 'POST',
  2058.                         headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
  2059.                     })
  2060.                     .then(response => {
  2061.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  2062.                         return response.json();
  2063.                     })
  2064.                     .then(data => {
  2065.                         alert(data.success ? data.data.message : 'Error: ' + data.data);
  2066.                         updateStatus();
  2067.                     })
  2068.                     .catch(error => {
  2069.                         console.error('Error converting post images:', error);
  2070.                         alert('Failed to convert post images: ' + error.message);
  2071.                     });
  2072.                 }
  2073.             });
  2074.  
  2075.             document.getElementById('run-all').addEventListener('click', () => {
  2076.                 if (confirm('<?php echo esc_js(__('Run all steps?', 'wpturbo')); ?>')) {
  2077.                     isConverting = true;
  2078.                     document.getElementById('stop-conversion').style.display = 'inline-block';
  2079.                     fetch('<?php echo admin_url('admin.php?page=webp-converter&convert_existing_images_to_webp=1'); ?>')
  2080.                         .then(response => {
  2081.                             if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  2082.                             return response;
  2083.                         })
  2084.                         .then(() => {
  2085.                             convertNextImage(0);
  2086.                             return new Promise(resolve => {
  2087.                                 const checkComplete = setInterval(() => {
  2088.                                     fetch('<?php echo admin_url('admin-ajax.php?action=webp_status&nonce=' . wp_create_nonce('webp_converter_nonce')); ?>')
  2089.                                         .then(response => response.json())
  2090.                                         .then(data => {
  2091.                                             updateStatus();
  2092.                                             if (data.complete) {
  2093.                                                 clearInterval(checkComplete);
  2094.                                                 resolve();
  2095.                                             }
  2096.                                         })
  2097.                                         .catch(error => {
  2098.                                             console.error('Error checking conversion status:', error);
  2099.                                             clearInterval(checkComplete);
  2100.                                             resolve();
  2101.                                         });
  2102.                                 }, 1000);
  2103.                             });
  2104.                         })
  2105.                         .then(() => {
  2106.                             return fetch('<?php echo admin_url('admin-ajax.php?action=convert_post_images_to_webp&nonce=' . wp_create_nonce('webp_converter_nonce')); ?>', {
  2107.                                 method: 'POST',
  2108.                                 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
  2109.                             })
  2110.                             .then(response => {
  2111.                                 if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  2112.                                 return response.json();
  2113.                             })
  2114.                             .then(data => {
  2115.                                 updateStatus();
  2116.                                 alert(data.success ? data.data.message : 'Error: ' + data.data);
  2117.                             });
  2118.                         })
  2119.                         .then(() => {
  2120.                             return fetch('<?php echo admin_url('admin.php?page=webp-converter&cleanup_leftover_originals=1'); ?>');
  2121.                         })
  2122.                         .then(() => {
  2123.                             isConverting = false;
  2124.                             document.getElementById('stop-conversion').style.display = 'none';
  2125.                             updateStatus();
  2126.                             alert('<?php echo esc_js(__('All steps completed!', 'wpturbo')); ?>');
  2127.                         })
  2128.                         .catch(error => {
  2129.                             console.error('Error in Run All:', error);
  2130.                             alert('Run All failed: ' + error.message);
  2131.                             isConverting = false;
  2132.                             document.getElementById('stop-conversion').style.display = 'none';
  2133.                         });
  2134.                 }
  2135.             });
  2136.  
  2137.             document.getElementById('stop-conversion').addEventListener('click', () => {
  2138.                 isConverting = false;
  2139.                 document.getElementById('stop-conversion').style.display = 'none';
  2140.             });
  2141.  
  2142.             document.getElementById('clear-log').addEventListener('click', () => {
  2143.                 fetch('<?php echo admin_url('admin.php?page=webp-converter&clear_log=1'); ?>')
  2144.                     .then(response => {
  2145.                         if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  2146.                         return response;
  2147.                     })
  2148.                     .then(() => updateStatus())
  2149.                     .catch(error => {
  2150.                         console.error('Error clearing log:', error);
  2151.                         alert('Failed to clear log: ' + error.message);
  2152.                     });
  2153.             });
  2154.  
  2155.             document.getElementById('reset-defaults').addEventListener('click', () => {
  2156.                 if (confirm('<?php echo esc_js(__('Reset all settings to defaults?', 'wpturbo')); ?>')) {
  2157.                     fetch('<?php echo admin_url('admin.php?page=webp-converter&reset_defaults=1'); ?>')
  2158.                         .then(response => {
  2159.                             if (!response.ok) throw new Error('Network response was not ok: ' + response.statusText);
  2160.                             return response;
  2161.                         })
  2162.                         .then(() => updateStatus())
  2163.                         .catch(error => {
  2164.                             console.error('Error resetting defaults:', error);
  2165.                             alert('Failed to reset defaults: ' + error.message);
  2166.                         });
  2167.                 }
  2168.             });
  2169.  
  2170.             document.getElementById('export-media-zip').addEventListener('click', () => {
  2171.                 if (confirm('<?php echo esc_js(__('Export all media as a ZIP file?', 'wpturbo')); ?>')) {
  2172.                     const url = '<?php echo admin_url('admin-ajax.php?action=webp_export_media_zip&nonce=' . wp_create_nonce('webp_converter_nonce')); ?>';
  2173.                     window.location.href = url;
  2174.                 }
  2175.             });
  2176.             <?php endif; ?>
  2177.  
  2178.             updateStatus();
  2179.         });
  2180.     </script>
  2181.     <?php
  2182. }
  2183.  
  2184. // Setup AJAX hooks
  2185. add_action('admin_init', function() {
  2186.     add_action('wp_ajax_webp_status', 'wpturbo_webp_conversion_status');
  2187.     add_action('wp_ajax_webp_convert_single', 'wpturbo_convert_single_image');
  2188.     add_action('wp_ajax_webp_export_media_zip', 'wpturbo_export_media_zip');
  2189.     if (isset($_GET['convert_existing_images_to_webp']) && current_user_can('manage_options')) {
  2190.         delete_option('webp_conversion_complete');
  2191.     }
  2192. });
  2193.  
  2194. // Admin notices
  2195. add_action('admin_notices', function() {
  2196.     if (isset($_GET['convert_existing_images_to_webp'])) {
  2197.         echo '<div class="notice notice-success"><p>' . __('Conversion started. Monitor progress in Media.', 'wpturbo') . '</p></div>';
  2198.     }
  2199.     if (isset($_GET['set_max_width']) && wpturbo_set_max_widths()) {
  2200.         echo '<div class="notice notice-success"><p>' . __('Max widths updated.', 'wpturbo') . '</p></div>';
  2201.     }
  2202.     if (isset($_GET['set_max_height']) && wpturbo_set_max_heights()) {
  2203.         echo '<div class="notice notice-success"><p>' . __('Max heights updated.', 'wpturbo') . '</p></div>';
  2204.     }
  2205.     if (isset($_GET['reset_defaults']) && wpturbo_reset_defaults()) {
  2206.         echo '<div class="notice notice-success"><p>' . __('Settings reset to defaults.', 'wpturbo') . '</p></div>';
  2207.     }
  2208.     if (isset($_GET['set_min_size_kb']) && wpturbo_set_min_size_kb()) {
  2209.         echo '<div class="notice notice-success"><p>' . __('Minimum size threshold updated.', 'wpturbo') . '</p></div>';
  2210.     }
  2211.     if (isset($_GET['set_use_avif']) && wpturbo_set_use_avif()) {
  2212.         echo '<div class="notice notice-success"><p>' . __('Conversion format updated. Please reconvert all images.', 'wpturbo') . '</p></div>';
  2213.     }
  2214. });
  2215.  
  2216. // Custom image size names
  2217. add_filter('image_size_names_choose', 'wpturbo_disable_default_sizes', 999);
  2218. function wpturbo_disable_default_sizes($sizes) {
  2219.     $mode = wpturbo_get_resize_mode();
  2220.     $max_values = ($mode === 'width') ? wpturbo_get_max_widths() : wpturbo_get_max_heights();
  2221.     $custom_sizes = ['thumbnail' => __('Thumbnail (150x150)', 'wpturbo')];
  2222.     $additional_values = array_slice($max_values, 1, 3);
  2223.     foreach ($additional_values as $value) {
  2224.         $custom_sizes["custom-$value"] = ($mode === 'width') ? sprintf(__('Custom %dpx Width', 'wpturbo'), $value) : sprintf(__('Custom %dpx Height', 'wpturbo'), $value);
  2225.     }
  2226.     return $custom_sizes;
  2227. }
  2228.  
  2229. // Disable scaling
  2230. add_filter('big_image_size_threshold', '__return_false', 999);
  2231.  
  2232. // Clean up attachment files on deletion
  2233. add_action('wp_delete_attachment', 'wpturbo_delete_attachment_files', 10, 1);
  2234. function wpturbo_delete_attachment_files($attachment_id) {
  2235.     if (in_array($attachment_id, wpturbo_get_excluded_images())) return;
  2236.  
  2237.     $file = get_attached_file($attachment_id);
  2238.     if ($file && file_exists($file)) @unlink($file);
  2239.  
  2240.     $metadata = wp_get_attachment_metadata($attachment_id);
  2241.     if ($metadata && isset($metadata['sizes'])) {
  2242.         $upload_dir = wp_upload_dir()['basedir'];
  2243.         foreach ($metadata['sizes'] as $size) {
  2244.             $size_file = $upload_dir . '/' . dirname($metadata['file']) . '/' . $size['file'];
  2245.             if (file_exists($size_file)) @unlink($size_file);
  2246.         }
  2247.     }
  2248. }
  2249.  
  2250. // Ensure MIME types on plugin activation or format switch
  2251. register_activation_hook(__FILE__, 'wpturbo_ensure_mime_types');
  2252. add_action('update_option_webp_use_avif', 'wpturbo_ensure_mime_types');
  2253.  
  2254.  
  2255. // Stop Processed Images from being Processed again unless Settings Change
  2256. add_action('admin_init', function () {
  2257.     if (!current_user_can('manage_options') || !isset($_GET['patch_pixrefiner_stamp'])) return;
  2258.  
  2259.     $attachments = get_posts([
  2260.         'post_type' => 'attachment',
  2261.         'post_mime_type' => ['image/webp', 'image/avif'],
  2262.         'posts_per_page' => -1,
  2263.         'fields' => 'ids',
  2264.     ]);
  2265.  
  2266.     $expected_stamp = [
  2267.         'format'      => wpturbo_get_use_avif() ? 'avif' : 'webp',
  2268.         'quality'     => wpturbo_get_quality(),
  2269.         'resize_mode' => wpturbo_get_resize_mode(),
  2270.         'max_values'  => (wpturbo_get_resize_mode() === 'width') ? wpturbo_get_max_widths() : wpturbo_get_max_heights(),
  2271.     ];
  2272.  
  2273.     foreach ($attachments as $id) {
  2274.         $meta = wp_get_attachment_metadata($id);
  2275.         if (empty($meta['pixrefiner_stamp'])) {
  2276.             $meta['pixrefiner_stamp'] = $expected_stamp;
  2277.             wp_update_attachment_metadata($id, $meta);
  2278.         }
  2279.     }
  2280.  
  2281.     echo "<div class='notice notice-success'><p>✅ PixRefiner stamp patch complete.</p></div>";
  2282. });
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement