diff --git a/image_optimizer_nat_OC_20_22_1_2_0.ocmod.zip b/image_optimizer_nat_OC_20_22_1_2_0.ocmod.zip index 39dff49..43d8091 100644 Binary files a/image_optimizer_nat_OC_20_22_1_2_0.ocmod.zip and b/image_optimizer_nat_OC_20_22_1_2_0.ocmod.zip differ diff --git a/upload/admin/controller/module/img_opti.php b/upload/admin/controller/module/img_opti.php index b2785f7..bf93a79 100644 --- a/upload/admin/controller/module/img_opti.php +++ b/upload/admin/controller/module/img_opti.php @@ -6,7 +6,11 @@ class ControllerModuleImgOpti extends Controller { private $translations = array(); public function index() { - $data = $this->load->language('module/img_opti'); + $lang_data = $this->load->language('module/img_opti'); + if (!is_array($lang_data)) { + $lang_data = array(); + } + $data = array_merge($this->loadLanguageSafe(), $lang_data); $this->document->setTitle($this->language->get('heading_title_raw')); $this->load->model('setting/setting'); @@ -95,6 +99,7 @@ class ControllerModuleImgOpti extends Controller { 'module_img_opti_cron_webp_quality' => 80, 'module_img_opti_cron_max_width' => 1600, 'module_img_opti_cron_max_height' => 1600, + 'module_img_opti_cron_batch_limit' => 100, 'module_img_opti_cron_tasks' => '', 'module_img_opti_wm_type' => 'image', 'module_img_opti_wm_image' => '', @@ -145,9 +150,12 @@ class ControllerModuleImgOpti extends Controller { 'module_img_opti_disk_quota_total' => 0, 'module_img_opti_disk_quota_used' => 0, 'module_img_opti_disk_status' => 0, + 'module_img_opti_grid_limit' => 50, 'module_img_opti_wm_max_width' => 0, 'module_img_opti_wm_max_height' => 0, - 'module_img_opti_truefilemanager_webp'=> (is_file(DIR_APPLICATION . 'controller/common/truefilemanager.php') || is_file(DIR_SYSTEM . 'library/sitecreator/trueFileManager/ControllerCommonTrueFileManager.php')) ? 1 : 0 + 'module_img_opti_truefilemanager_webp'=> (is_file(DIR_APPLICATION . 'controller/common/truefilemanager.php') || is_file(DIR_SYSTEM . 'library/sitecreator/trueFileManager/ControllerCommonTrueFileManager.php')) ? 1 : 0, + 'module_img_opti_auto_fake_jpg_guard' => 1, + 'module_img_opti_fake_jpg_mode' => 'jpg' ); foreach ($fields as $key => $default) { @@ -175,10 +183,22 @@ class ControllerModuleImgOpti extends Controller { 'jpg_quality' => (int)$data['module_img_opti_cron_jpg_quality'], 'webp_quality' => (int)$data['module_img_opti_cron_webp_quality'], 'max_width' => (int)$data['module_img_opti_cron_max_width'], - 'max_height' => (int)$data['module_img_opti_cron_max_height'] + 'max_height' => (int)$data['module_img_opti_cron_max_height'], + 'batch_limit' => (int)$data['module_img_opti_cron_batch_limit'] ) ); $data['module_img_opti_cron_tasks_json'] = json_encode($default_cron_tasks, JSON_UNESCAPED_UNICODE); + } else { + $cron_tasks_arr = json_decode(html_entity_decode($data['module_img_opti_cron_tasks'], ENT_QUOTES, 'UTF-8'), true); + if (is_array($cron_tasks_arr)) { + foreach ($cron_tasks_arr as &$t) { + if (!isset($t['batch_limit'])) { + $t['batch_limit'] = 100; + } + } + unset($t); + $data['module_img_opti_cron_tasks_json'] = json_encode($cron_tasks_arr, JSON_UNESCAPED_UNICODE); + } } if (empty($data['module_img_opti_wm_rules'])) { @@ -431,6 +451,11 @@ class ControllerModuleImgOpti extends Controller { } } + $stats = $this->getModuleModel()->getGlobalStats(); + $data['stats_files'] = number_format($stats['total_files'], 0, '.', ' '); + $data['stats_saved_mb'] = number_format($stats['saved_mb'], 2, '.', ' '); + $data['stats_avg_pct'] = number_format($stats['avg_pct'], 1, '.', ' '); + $data['header'] = $this->load->controller('common/header'); $data['column_left'] = $this->load->controller('common/column_left'); $data['footer'] = $this->load->controller('common/footer'); @@ -438,6 +463,13 @@ class ControllerModuleImgOpti extends Controller { $this->response->setOutput($this->load->view('module/img_opti', $data)); } + private function getModuleModel() { + if (!isset($this->model_extension_module_img_opti)) { + $this->load->model('module/img_opti'); + } + return $this->model_extension_module_img_opti; + } + public function autocomplete_category() { $this->loadLanguageSafe(); if (!isset($this->request->get['user_token']) || !isset($this->session->data['user_token']) || $this->request->get['user_token'] !== $this->session->data['user_token']) { @@ -553,10 +585,35 @@ class ControllerModuleImgOpti extends Controller { file_put_contents(DIR_CACHE . 'img_opti_queue.json', json_encode($allFiles, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); $json['total'] = count($allFiles); $json['success'] = $this->language->get('text_found') . $json['total']; + $json['log'] = array("
" . htmlspecialchars($json['success'], ENT_QUOTES | ENT_IGNORE, 'UTF-8') . "
"); + file_put_contents(DIR_CACHE . 'img_opti_opti_scan_summary.json', json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); + @unlink(DIR_CACHE . 'img_opti_process_state.json'); $this->response->setOutput(json_encode($json)); } + public function get_cached_opti() { + $this->loadLanguageSafe(); + $this->response->addHeader('Content-Type: application/json'); + $state_file = DIR_CACHE . 'img_opti_process_state.json'; + if (file_exists($state_file)) { + $state = json_decode(file_get_contents($state_file), true); + if ($state) { + $this->response->setOutput(json_encode($state)); + return; + } + } + $summary_file = DIR_CACHE . 'img_opti_opti_scan_summary.json'; + if (file_exists($summary_file)) { + $json = json_decode(file_get_contents($summary_file), true); + if ($json) { + $this->response->setOutput(json_encode($json)); + return; + } + } + $this->response->setOutput(json_encode(array('total' => 0, 'success' => ''))); + } + public function process() { $this->loadLanguageSafe(); if (!isset($this->request->get['user_token']) || !isset($this->session->data['user_token']) || $this->request->get['user_token'] !== $this->session->data['user_token']) { @@ -577,6 +634,7 @@ class ControllerModuleImgOpti extends Controller { session_write_close(); if (!file_exists($queueFile)) { + $this->markProcessFinishedState(); $json['finished'] = true; $this->response->setOutput(json_encode($json)); return; @@ -601,6 +659,7 @@ class ControllerModuleImgOpti extends Controller { flock($fp, LOCK_UN); fclose($fp); @unlink($queueFile); + $this->markProcessFinishedState(); $json['finished'] = true; $this->response->setOutput(json_encode($json)); return; @@ -803,14 +862,63 @@ class ControllerModuleImgOpti extends Controller { $json['optimized'] = $batchOptimized; $json['batch_old'] = $batchOldSize; $json['batch_new'] = $batchNewSize; + + if ($batchOptimized > 0 && $batchOldSize > $batchNewSize) { + $bytesSaved = $batchOldSize - $batchNewSize; + $this->getModuleModel()->recordGlobalStats($batchOptimized, $bytesSaved, $batchOldSize, 'manual_opti'); + } + $globalStats = $this->getModuleModel()->getGlobalStats(); + $json['stats'] = array( + 'files' => number_format($globalStats['total_files'], 0, '.', ' '), + 'saved_mb' => number_format($globalStats['saved_mb'], 2, '.', ' '), + 'avg_pct' => number_format($globalStats['avg_pct'], 1, '.', ' ') + ); - if (!file_exists($queueFile)) { + if (!file_exists($queueFile) || empty($queue)) { $json['finished'] = true; } + + $stateFile = DIR_CACHE . 'img_opti_process_state.json'; + $state = array('total' => 0, 'processed' => 0, 'optimized' => 0, 'old_bytes' => 0, 'new_bytes' => 0, 'finished' => false, 'log' => array(), 'mode' => $mode, 'copy_folder' => $copyFolder); + if (file_exists($stateFile)) { + $prev = json_decode(file_get_contents($stateFile), true); + if ($prev) $state = array_merge($prev, $state); + } + $state['mode'] = $mode; + $state['copy_folder'] = $copyFolder; + $state['processed'] += $processed; + $state['optimized'] += $batchOptimized; + $state['old_bytes'] += $batchOldSize; + $state['new_bytes'] += $batchNewSize; + if (!empty($json['log'])) { + $state['log'] = array_merge($state['log'], $json['log']); + } + if (!empty($json['finished'])) { + $state['finished'] = true; + } + $scanSummary = DIR_CACHE . 'img_opti_opti_scan_summary.json'; + if (file_exists($scanSummary)) { + $scan = json_decode(file_get_contents($scanSummary), true); + if ($scan && isset($scan['total'])) { + $state['total'] = $scan['total']; + } + } + file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); $this->response->setOutput(json_encode($json)); } + private function markProcessFinishedState() { + $stateFile = DIR_CACHE . 'img_opti_process_state.json'; + if (file_exists($stateFile)) { + $state = json_decode(file_get_contents($stateFile), true); + if ($state) { + $state['finished'] = true; + file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); + } + } + } + public function applyChanges() { $this->loadLanguageSafe(); if (!isset($this->request->get['user_token']) || !isset($this->session->data['user_token']) || $this->request->get['user_token'] !== $this->session->data['user_token']) { @@ -934,8 +1042,6 @@ class ControllerModuleImgOpti extends Controller { $this->saveOptimizedCache($cache); } - $this->recursiveDelete($source_dir); - $msg = $this->language->get('text_apply_success'); $msg .= $do_backup ? $this->language->get('text_apply_backup_ok') . htmlspecialchars($backup_name, ENT_QUOTES, 'UTF-8') : $this->language->get('text_apply_no_backup'); @@ -945,6 +1051,10 @@ class ControllerModuleImgOpti extends Controller { $json['success'] = $msg; } + @unlink(DIR_CACHE . 'img_opti_process_state.json'); + @unlink(DIR_CACHE . 'img_opti_opti_scan_summary.json'); + @unlink(DIR_CACHE . 'img_opti_queue.json'); + $this->response->setOutput(json_encode($json)); } @@ -1011,6 +1121,41 @@ class ControllerModuleImgOpti extends Controller { $json['success'] = $this->language->get('text_quarantine_empty'); + @unlink(DIR_CACHE . 'img_opti_process_state.json'); + @unlink(DIR_CACHE . 'img_opti_opti_scan_summary.json'); + @unlink(DIR_CACHE . 'img_opti_queue.json'); + + $this->response->setOutput(json_encode($json)); + } + + public function save_grid_limit() { + $this->loadLanguageSafe(); + $json = array(); + + if (!isset($this->request->get['user_token']) || !isset($this->session->data['user_token']) || $this->request->get['user_token'] !== $this->session->data['user_token']) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + $limit = isset($this->request->post['limit']) ? max(1, (int)$this->request->post['limit']) : 50; + + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "setting` WHERE `code` = 'module_img_opti' AND `key` = 'module_img_opti_grid_limit' AND `store_id` = '0'"); + if ($query->num_rows) { + $this->db->query("UPDATE `" . DB_PREFIX . "setting` SET `value` = '" . (int)$limit . "' WHERE `code` = 'module_img_opti' AND `key` = 'module_img_opti_grid_limit' AND `store_id` = '0'"); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "setting` SET `code` = 'module_img_opti', `key` = 'module_img_opti_grid_limit', `value` = '" . (int)$limit . "', `serialized` = '0', `store_id` = '0'"); + } + + $json['success'] = true; + $json['limit'] = $limit; + $this->response->setOutput(json_encode($json)); } @@ -1099,7 +1244,7 @@ class ControllerModuleImgOpti extends Controller { $tool = isset($this->request->post['tool']) ? preg_replace('/[^a-zA-Z0-9_]/', '', (string)$this->request->post['tool']) : ''; $this->load->model('module/img_opti'); - $json = $this->model_module_img_opti->generateCompareUrls($tool, $id); + $json = $this->model_extension_module_img_opti->generateCompareUrls($tool, $id); $this->response->addHeader('Content-Type: application/json'); $this->response->setOutput(json_encode($json)); @@ -1362,6 +1507,23 @@ class ControllerModuleImgOpti extends Controller { return false; } + public function reset_stats() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['user_token']) || !isset($this->session->data['user_token']) || $this->request->get['user_token'] !== $this->session->data['user_token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + $newStats = $this->getModuleModel()->resetGlobalStats(); + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array( + 'success' => true, + 'files' => number_format($newStats['total_files'], 0, '.', ' '), + 'saved_mb' => number_format($newStats['saved_mb'], 2, '.', ' '), + 'avg_pct' => number_format($newStats['avg_pct'], 1, '.', ' ') + ))); + } + public function run_tool() { $this->loadLanguageSafe(); if (!isset($this->request->get['user_token']) || !isset($this->session->data['user_token']) || $this->request->get['user_token'] !== $this->session->data['user_token']) { @@ -1391,12 +1553,19 @@ class ControllerModuleImgOpti extends Controller { $this->load->model('module/img_opti'); - $method = $action . '_' . $tool; - if ($tool === 'broken') { - $method = $action . '_broken_db'; + if ($action === 'get_cached_grid') { + $method = 'get_cached_grid'; + } elseif ($action === 'preview_black_bg_row') { + $method = 'preview_black_bg_row'; + } else { + $method = $action . '_' . $tool; + if ($tool === 'broken') { + $method = $action . '_broken_db'; + } } $allowed_methods = array( + 'get_cached_grid', 'scan_broken_db', 'fix_broken_db', 'scan_broken_files', 'fix_broken_files', 'scan_duplicates', 'fix_duplicates', @@ -1409,15 +1578,33 @@ class ControllerModuleImgOpti extends Controller { 'scan_placeholders', 'fix_placeholders', 'scan_watermark', 'scan_small_photos', 'fix_small_photos', - 'scan_html_broken', + 'scan_html_broken', 'fix_html_broken', 'scan_folder_tree', 'fix_folder_tree', 'scan_sanitizer', 'fix_sanitizer', 'scan_cmyk', 'fix_cmyk', - 'scan_heavy_files', 'fix_heavy_files' + 'scan_heavy_files', 'fix_heavy_files', + 'scan_svg_security', 'fix_svg_security', + 'scan_black_bg', 'fix_black_bg', 'preview_black_bg_row' ); if (in_array($method, $allowed_methods)) { - $json = $this->model_module_img_opti->{$method}($this->request->post); + $start = isset($this->request->post['start']) ? max(0, (int)$this->request->post['start']) : 0; + $limit = isset($this->request->post['limit']) ? max(0, (int)$this->request->post['limit']) : 0; + $page = isset($this->request->post['page']) ? max(1, (int)$this->request->post['page']) : 1; + + if ($limit > 0 && $start === 0 && $page > 1) { + $start = ($page - 1) * $limit; + } + + $this->request->post['start'] = $start; + $this->request->post['limit'] = $limit; + + $json = $this->model_extension_module_img_opti->{$method}($this->request->post); + + if (isset($json['total']) && $limit > 0) { + $json['page'] = floor($start / $limit) + 1; + $json['pages'] = max(1, ceil($json['total'] / $limit)); + } } else { $json['error'] = 'Tool method not found: ' . $method; } @@ -1500,7 +1687,7 @@ class ControllerModuleImgOpti extends Controller { 'limit' => isset($this->request->post['limit']) ? (int)$this->request->post['limit'] : 200 ); - $json = $this->model_module_img_opti->scan_broken_db($data); + $json = $this->model_extension_module_img_opti->scan_broken_db($data); $this->response->addHeader('Content-Type: application/json'); $this->response->setOutput(json_encode($json)); } @@ -1536,7 +1723,7 @@ class ControllerModuleImgOpti extends Controller { 'fix_all' => isset($this->request->post['fix_all']) ? (int)$this->request->post['fix_all'] : 0 ); - $this->model_module_img_opti->fix_broken_db($data); + $this->model_extension_module_img_opti->fix_broken_db($data); $json['success'] = true; $this->response->addHeader('Content-Type: application/json'); @@ -1743,9 +1930,24 @@ class ControllerModuleImgOpti extends Controller { file_put_contents(DIR_CACHE . 'img_opti_trash_queue.json', json_encode($trashFiles, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); $json['total'] = count($trashFiles); + file_put_contents(DIR_CACHE . 'img_opti_trash_scan_summary.json', json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); $this->response->setOutput(json_encode($json)); } + public function get_cached_trash() { + $this->loadLanguageSafe(); + $this->response->addHeader('Content-Type: application/json'); + $summary_file = DIR_CACHE . 'img_opti_trash_scan_summary.json'; + if (file_exists($summary_file)) { + $json = json_decode(file_get_contents($summary_file), true); + if ($json) { + $this->response->setOutput(json_encode($json)); + return; + } + } + $this->response->setOutput(json_encode(array('total' => 0, 'total_bytes' => 0, 'log' => array()))); + } + public function process_trash() { $this->loadLanguageSafe(); if (!isset($this->request->get['user_token']) || !isset($this->session->data['user_token']) || $this->request->get['user_token'] !== $this->session->data['user_token']) { @@ -1840,6 +2042,7 @@ class ControllerModuleImgOpti extends Controller { file_put_contents($queueFile, json_encode($queue, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); if (empty($queue)) { @unlink($queueFile); + @unlink(DIR_CACHE . 'img_opti_trash_scan_summary.json'); $json['finished'] = true; } $this->response->setOutput(json_encode($json)); @@ -2014,20 +2217,34 @@ class ControllerModuleImgOpti extends Controller { } } - $temp_preview = DIR_IMAGE . 'cache/temp_wm_preview.png'; $compare_dir = DIR_IMAGE . 'cache/'; if (!is_dir($compare_dir)) { @mkdir($compare_dir, 0755, true); } - $preview_w = 500; - $preview_h = 500; - $preview_im = imagecreatetruecolor($preview_w, $preview_h); + // Clean up old preview files older than 30 seconds + foreach (@glob($compare_dir . 'temp_wm_preview_*.png') as $old_file) { + if (is_file($old_file) && (time() - filemtime($old_file) > 30)) { + @unlink($old_file); + } + } + + $preview_hash = md5(microtime(true) . rand(10000, 99999)); + $temp_rel_path = 'cache/temp_wm_preview_' . $preview_hash . '.png'; + $temp_preview = DIR_IMAGE . $temp_rel_path; if ($bg_mode === 'white') { + $preview_w = 500; + $preview_h = 500; + $preview_im = imagecreatetruecolor($preview_w, $preview_h); $white = imagecolorallocate($preview_im, 255, 255, 255); imagefill($preview_im, 0, 0, $white); } elseif ($bg_mode === 'transparent') { + $preview_w = 500; + $preview_h = 500; + $preview_im = imagecreatetruecolor($preview_w, $preview_h); + imagealphablending($preview_im, false); + imagesavealpha($preview_im, true); $c1 = imagecolorallocate($preview_im, 255, 255, 255); $c2 = imagecolorallocate($preview_im, 226, 232, 240); imagefill($preview_im, 0, 0, $c1); @@ -2040,9 +2257,6 @@ class ControllerModuleImgOpti extends Controller { } } } else { - $white = imagecolorallocate($preview_im, 255, 255, 255); - imagefill($preview_im, 0, 0, $white); - $base_img = false; if (is_file($base_file)) { $ext = strtolower(pathinfo($base_file, PATHINFO_EXTENSION)); @@ -2058,14 +2272,20 @@ class ControllerModuleImgOpti extends Controller { if ($base_img) { $orig_w = imagesx($base_img); $orig_h = imagesy($base_img); - $scale = min($preview_w / $orig_w, $preview_h / $orig_h); - $new_w = intval($orig_w * $scale); - $new_h = intval($orig_h * $scale); - $dst_x = intval(($preview_w - $new_w) / 2); - $dst_y = intval(($preview_h - $new_h) / 2); + $max_side = 500; + $scale = min($max_side / max(1, $orig_w), $max_side / max(1, $orig_h)); + $preview_w = max(10, intval($orig_w * $scale)); + $preview_h = max(10, intval($orig_h * $scale)); - imagecopyresampled($preview_im, $base_img, $dst_x, $dst_y, 0, 0, $new_w, $new_h, $orig_w, $orig_h); + $preview_im = imagecreatetruecolor($preview_w, $preview_h); + imagecopyresampled($preview_im, $base_img, 0, 0, 0, 0, $preview_w, $preview_h, $orig_w, $orig_h); imagedestroy($base_img); + } else { + $preview_w = 500; + $preview_h = 500; + $preview_im = imagecreatetruecolor($preview_w, $preview_h); + $white = imagecolorallocate($preview_im, 255, 255, 255); + imagefill($preview_im, 0, 0, $white); } } @@ -2093,12 +2313,12 @@ class ControllerModuleImgOpti extends Controller { $this->config->set('module_img_opti_wm_target_blog', 0); $this->load->model('module/img_opti'); - $res = $this->model_module_img_opti->applyDynamicWatermark($temp_preview, '', true); + $res = $this->model_extension_module_img_opti->applyDynamicWatermark($temp_preview, '', true); if ($res) { $catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : ''); $json['success'] = true; - $json['preview_url'] = $catalog_url . 'image/cache/temp_wm_preview.png?t=' . time(); + $json['preview_url'] = $catalog_url . 'image/' . $temp_rel_path . '?v=' . microtime(true); } else { $json['error'] = $this->language->get('error_wm_preview') ?: 'Failed to generate preview'; } @@ -2373,12 +2593,69 @@ class ControllerModuleImgOpti extends Controller { die('Permission denied'); } $this->load->model('setting/setting'); - $settings = $this->model_setting_setting->getSetting('module_img_opti'); + $rawSettings = $this->model_setting_setting->getSetting('module_img_opti'); + + $allKeys = array( + 'module_img_opti_status', + 'module_img_opti_engine', + 'module_img_opti_jpg_quality', + 'module_img_opti_png_quality', + 'module_img_opti_webp_quality', + 'module_img_opti_max_width', + 'module_img_opti_max_height', + 'module_img_opti_convert_webp', + 'module_img_opti_support_svg', + 'module_img_opti_support_webp', + 'module_img_opti_webp_on_fly', + 'module_img_opti_lazy_load', + 'module_img_opti_auto_fake_jpg_guard', + 'module_img_opti_fake_jpg_mode', + 'module_img_opti_broken_fallback', + 'module_img_opti_fallback_image', + 'module_img_opti_cron_tasks', + 'module_img_opti_wm_status', + 'module_img_opti_wm_rules', + 'module_img_opti_wm_max_width', + 'module_img_opti_wm_max_height', + 'module_img_opti_ph_rules', + 'module_img_opti_folder_rules', + 'module_img_opti_log_level', + 'module_img_opti_min_savings_bytes', + 'module_img_opti_min_savings_percent', + 'module_img_opti_heavy_limit', + 'module_img_opti_heavy_min_size', + 'module_img_opti_png_jpg_limit', + 'module_img_opti_png_jpg_min_size', + 'module_img_opti_sanitizer_limit', + 'module_img_opti_grid_limit', + 'module_img_opti_disk_quota_total', + 'module_img_opti_disk_quota_used', + 'module_img_opti_disk_status', + 'module_img_opti_truefilemanager_webp' + ); + + $exportSettings = array(); + foreach ($allKeys as $k) { + if (isset($rawSettings[$k])) { + $exportSettings[$k] = $rawSettings[$k]; + } else { + $exportSettings[$k] = $this->config->get($k); + } + } + + if (is_array($rawSettings)) { + foreach ($rawSettings as $k => $v) { + if (strpos($k, 'module_img_opti') === 0 && !isset($exportSettings[$k])) { + $exportSettings[$k] = $v; + } + } + } + $export = array( '_module' => 'img_opti', '_version' => '1.2.0', '_exported' => date('Y-m-d H:i:s'), - 'settings' => $settings + 'settings' => $exportSettings ); $filename = 'img_opti_settings_' . date('Ymd_His') . '.json'; $this->response->addHeader('Content-Type: application/json'); @@ -2408,13 +2685,16 @@ class ControllerModuleImgOpti extends Controller { $json['error'] = $this->language->get('error_import_invalid'); } else { $this->load->model('setting/setting'); - $safe = array(); + $current = $this->model_setting_setting->getSetting('module_img_opti'); + if (!is_array($current)) { + $current = array(); + } foreach ($data['settings'] as $key => $value) { if (strpos($key, 'module_img_opti') === 0) { - $safe[$key] = $value; + $current[$key] = $value; } } - $this->model_setting_setting->editSetting('module_img_opti', $safe); + $this->model_setting_setting->editSetting('module_img_opti', $current); $this->cache->delete('img_opti_disk_stats'); $json['success'] = true; } @@ -2422,5 +2702,158 @@ class ControllerModuleImgOpti extends Controller { $this->response->addHeader('Content-Type: application/json'); $this->response->setOutput(json_encode($json)); } + + public function autoRotateImage($filePath, $im = null) { + if (!function_exists('exif_read_data')) { + return $im; + } + $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + if (!in_array($ext, array('jpg', 'jpeg', 'tiff'))) { + return $im; + } + $exif = @exif_read_data($filePath); + if (!$exif || !isset($exif['Orientation'])) { + return $im; + } + $orientation = (int)$exif['Orientation']; + $angle = 0; + switch ($orientation) { + case 3: + $angle = 180; + break; + case 6: + $angle = -90; + break; + case 8: + $angle = 90; + break; + } + if ($angle !== 0) { + if ($im !== null) { + $im = @imagerotate($im, $angle, 0); + } else { + $img = @imagecreatefromjpeg($filePath); + if ($img) { + $img = @imagerotate($img, $angle, 0); + @imagejpeg($img, $filePath, 95); + @imagedestroy($img); + } + } + } + return $im; + } + + public function convertPngToJpgClean($sourcePngPath, $targetJpgPath, $quality = 100, $bgColorHex = '#ffffff') { + $img = @imagecreatefrompng($sourcePngPath); + if (!$img) return false; + + if (function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { + imagepalettetotruecolor($img); + } + + $width = imagesx($img); + $height = imagesy($img); + + $canvas = imagecreatetruecolor($width, $height); + + $hex = ltrim($bgColorHex, '#'); + if (strlen($hex) === 3) { + $r = hexdec(substr($hex, 0, 1) . substr($hex, 0, 1)); + $g = hexdec(substr($hex, 1, 1) . substr($hex, 1, 1)); + $b = hexdec(substr($hex, 2, 1) . substr($hex, 2, 1)); + } else { + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + } + + $bgColor = imagecolorallocate($canvas, $r, $g, $b); + imagefill($canvas, 0, 0, $bgColor); + imagealphablending($canvas, true); + + imagecopy($canvas, $img, 0, 0, 0, 0, $width, $height); + + $success = @imagejpeg($canvas, $targetJpgPath, $quality); + + imagedestroy($img); + imagedestroy($canvas); + + return $success; + } + + public function convertPngToWebpClean($sourcePngPath, $targetWebpPath, $quality = 80) { + if (!function_exists('imagewebp')) return false; + $img = @imagecreatefrompng($sourcePngPath); + if (!$img) return false; + + if (function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { + imagepalettetotruecolor($img); + } + + imagealphablending($img, false); + imagesavealpha($img, true); + + $success = @imagewebp($img, $targetWebpPath, $quality); + imagedestroy($img); + + return $success; + } + + public function normalizePngPalette($img) { + if ($img && function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { + imagepalettetotruecolor($img); + } + return $img; + } + + public function normalizeCmykToRgbJpeg($fullPath, $quality = 90) { + if (!is_file($fullPath)) return false; + + if (extension_loaded('imagick')) { + try { + $img = new \Imagick($fullPath); + if ($img->getImageColorspace() == \Imagick::COLORSPACE_CMYK) { + $img->transformImageColorspace(\Imagick::COLORSPACE_SRGB); + $img->setImageFormat('jpeg'); + $img->writeImage($fullPath); + $img->clear(); + $img->destroy(); + return true; + } + $img->clear(); + $img->destroy(); + } catch (\Exception $e) {} + } + + $info = @getimagesize($fullPath); + if (isset($info['channels']) && $info['channels'] == 4) { + $img = @imagecreatefromjpeg($fullPath); + if ($img) { + $width = imagesx($img); + $height = imagesy($img); + $rgbCanvas = imagecreatetruecolor($width, $height); + for ($x = 0; $x < $width; $x++) { + for ($y = 0; $y < $height; $y++) { + $pixel = imagecolorat($img, $x, $y); + $c = ($pixel >> 16) & 0xFF; + $m = ($pixel >> 8) & 0xFF; + $y_val = $pixel & 0xFF; + + $r = (int)(($c * $y_val) / 255); + $g = (int)(($m * $y_val) / 255); + $b = (int)($y_val); + + $color = imagecolorallocate($rgbCanvas, min(255, max(0, $r)), min(255, max(0, $g)), min(255, max(0, $b))); + imagesetpixel($rgbCanvas, $x, $y, $color); + } + } + @imagejpeg($rgbCanvas, $fullPath, $quality); + imagedestroy($img); + imagedestroy($rgbCanvas); + return true; + } + } + return false; + } } diff --git a/upload/admin/view/template/module/img_opti.tpl b/upload/admin/view/template/module/img_opti.tpl index b28eff0..c87ffad 100644 --- a/upload/admin/view/template/module/img_opti.tpl +++ b/upload/admin/view/template/module/img_opti.tpl @@ -3300,8 +3300,10 @@ $(document).ready(function() {
-   - + + + +