fix(watermark): add interactive HTML5 colorpicker, unique cache-busted preview filenames and exact product canvas aspect ratio

This commit is contained in:
nertyog 2026-08-07 05:16:40 +03:00
parent c7eec6d352
commit 2a9e4c3769
3 changed files with 472 additions and 37 deletions

View File

@ -6,7 +6,11 @@ class ControllerModuleImgOpti extends Controller {
private $translations = array(); private $translations = array();
public function index() { 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->document->setTitle($this->language->get('heading_title_raw'));
$this->load->model('setting/setting'); $this->load->model('setting/setting');
@ -95,6 +99,7 @@ class ControllerModuleImgOpti extends Controller {
'module_img_opti_cron_webp_quality' => 80, 'module_img_opti_cron_webp_quality' => 80,
'module_img_opti_cron_max_width' => 1600, 'module_img_opti_cron_max_width' => 1600,
'module_img_opti_cron_max_height' => 1600, 'module_img_opti_cron_max_height' => 1600,
'module_img_opti_cron_batch_limit' => 100,
'module_img_opti_cron_tasks' => '', 'module_img_opti_cron_tasks' => '',
'module_img_opti_wm_type' => 'image', 'module_img_opti_wm_type' => 'image',
'module_img_opti_wm_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_total' => 0,
'module_img_opti_disk_quota_used' => 0, 'module_img_opti_disk_quota_used' => 0,
'module_img_opti_disk_status' => 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_width' => 0,
'module_img_opti_wm_max_height' => 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) { foreach ($fields as $key => $default) {
@ -175,10 +183,22 @@ class ControllerModuleImgOpti extends Controller {
'jpg_quality' => (int)$data['module_img_opti_cron_jpg_quality'], 'jpg_quality' => (int)$data['module_img_opti_cron_jpg_quality'],
'webp_quality' => (int)$data['module_img_opti_cron_webp_quality'], 'webp_quality' => (int)$data['module_img_opti_cron_webp_quality'],
'max_width' => (int)$data['module_img_opti_cron_max_width'], '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); $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'])) { 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['header'] = $this->load->controller('common/header');
$data['column_left'] = $this->load->controller('common/column_left'); $data['column_left'] = $this->load->controller('common/column_left');
$data['footer'] = $this->load->controller('common/footer'); $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)); $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() { public function autocomplete_category() {
$this->loadLanguageSafe(); $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']) { 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)); 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['total'] = count($allFiles);
$json['success'] = $this->language->get('text_found') . $json['total']; $json['success'] = $this->language->get('text_found') . $json['total'];
$json['log'] = array("<div style='border-bottom:1px solid #333; padding:2px 0;'><span class='text-success'>" . htmlspecialchars($json['success'], ENT_QUOTES | ENT_IGNORE, 'UTF-8') . "</span></div>");
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)); $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() { public function process() {
$this->loadLanguageSafe(); $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']) { 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(); session_write_close();
if (!file_exists($queueFile)) { if (!file_exists($queueFile)) {
$this->markProcessFinishedState();
$json['finished'] = true; $json['finished'] = true;
$this->response->setOutput(json_encode($json)); $this->response->setOutput(json_encode($json));
return; return;
@ -601,6 +659,7 @@ class ControllerModuleImgOpti extends Controller {
flock($fp, LOCK_UN); flock($fp, LOCK_UN);
fclose($fp); fclose($fp);
@unlink($queueFile); @unlink($queueFile);
$this->markProcessFinishedState();
$json['finished'] = true; $json['finished'] = true;
$this->response->setOutput(json_encode($json)); $this->response->setOutput(json_encode($json));
return; return;
@ -804,13 +863,62 @@ class ControllerModuleImgOpti extends Controller {
$json['batch_old'] = $batchOldSize; $json['batch_old'] = $batchOldSize;
$json['batch_new'] = $batchNewSize; $json['batch_new'] = $batchNewSize;
if (!file_exists($queueFile)) { 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) || empty($queue)) {
$json['finished'] = true; $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)); $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() { public function applyChanges() {
$this->loadLanguageSafe(); $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']) { 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->saveOptimizedCache($cache);
} }
$this->recursiveDelete($source_dir);
$msg = $this->language->get('text_apply_success'); $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'); $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; $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)); $this->response->setOutput(json_encode($json));
} }
@ -1011,6 +1121,41 @@ class ControllerModuleImgOpti extends Controller {
$json['success'] = $this->language->get('text_quarantine_empty'); $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)); $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']) : ''; $tool = isset($this->request->post['tool']) ? preg_replace('/[^a-zA-Z0-9_]/', '', (string)$this->request->post['tool']) : '';
$this->load->model('module/img_opti'); $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->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($json)); $this->response->setOutput(json_encode($json));
@ -1362,6 +1507,23 @@ class ControllerModuleImgOpti extends Controller {
return false; 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() { public function run_tool() {
$this->loadLanguageSafe(); $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']) { 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'); $this->load->model('module/img_opti');
$method = $action . '_' . $tool; if ($action === 'get_cached_grid') {
if ($tool === 'broken') { $method = 'get_cached_grid';
$method = $action . '_broken_db'; } 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( $allowed_methods = array(
'get_cached_grid',
'scan_broken_db', 'fix_broken_db', 'scan_broken_db', 'fix_broken_db',
'scan_broken_files', 'fix_broken_files', 'scan_broken_files', 'fix_broken_files',
'scan_duplicates', 'fix_duplicates', 'scan_duplicates', 'fix_duplicates',
@ -1409,15 +1578,33 @@ class ControllerModuleImgOpti extends Controller {
'scan_placeholders', 'fix_placeholders', 'scan_placeholders', 'fix_placeholders',
'scan_watermark', 'scan_watermark',
'scan_small_photos', 'fix_small_photos', 'scan_small_photos', 'fix_small_photos',
'scan_html_broken', 'scan_html_broken', 'fix_html_broken',
'scan_folder_tree', 'fix_folder_tree', 'scan_folder_tree', 'fix_folder_tree',
'scan_sanitizer', 'fix_sanitizer', 'scan_sanitizer', 'fix_sanitizer',
'scan_cmyk', 'fix_cmyk', '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)) { 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 { } else {
$json['error'] = 'Tool method not found: ' . $method; $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 '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->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($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 '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; $json['success'] = true;
$this->response->addHeader('Content-Type: application/json'); $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)); 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); $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)); $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() { public function process_trash() {
$this->loadLanguageSafe(); $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']) { 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)); file_put_contents($queueFile, json_encode($queue, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR));
if (empty($queue)) { if (empty($queue)) {
@unlink($queueFile); @unlink($queueFile);
@unlink(DIR_CACHE . 'img_opti_trash_scan_summary.json');
$json['finished'] = true; $json['finished'] = true;
} }
$this->response->setOutput(json_encode($json)); $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/'; $compare_dir = DIR_IMAGE . 'cache/';
if (!is_dir($compare_dir)) { if (!is_dir($compare_dir)) {
@mkdir($compare_dir, 0755, true); @mkdir($compare_dir, 0755, true);
} }
$preview_w = 500; // Clean up old preview files older than 30 seconds
$preview_h = 500; foreach (@glob($compare_dir . 'temp_wm_preview_*.png') as $old_file) {
$preview_im = imagecreatetruecolor($preview_w, $preview_h); 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') { if ($bg_mode === 'white') {
$preview_w = 500;
$preview_h = 500;
$preview_im = imagecreatetruecolor($preview_w, $preview_h);
$white = imagecolorallocate($preview_im, 255, 255, 255); $white = imagecolorallocate($preview_im, 255, 255, 255);
imagefill($preview_im, 0, 0, $white); imagefill($preview_im, 0, 0, $white);
} elseif ($bg_mode === 'transparent') { } 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); $c1 = imagecolorallocate($preview_im, 255, 255, 255);
$c2 = imagecolorallocate($preview_im, 226, 232, 240); $c2 = imagecolorallocate($preview_im, 226, 232, 240);
imagefill($preview_im, 0, 0, $c1); imagefill($preview_im, 0, 0, $c1);
@ -2040,9 +2257,6 @@ class ControllerModuleImgOpti extends Controller {
} }
} }
} else { } else {
$white = imagecolorallocate($preview_im, 255, 255, 255);
imagefill($preview_im, 0, 0, $white);
$base_img = false; $base_img = false;
if (is_file($base_file)) { if (is_file($base_file)) {
$ext = strtolower(pathinfo($base_file, PATHINFO_EXTENSION)); $ext = strtolower(pathinfo($base_file, PATHINFO_EXTENSION));
@ -2058,14 +2272,20 @@ class ControllerModuleImgOpti extends Controller {
if ($base_img) { if ($base_img) {
$orig_w = imagesx($base_img); $orig_w = imagesx($base_img);
$orig_h = imagesy($base_img); $orig_h = imagesy($base_img);
$scale = min($preview_w / $orig_w, $preview_h / $orig_h); $max_side = 500;
$new_w = intval($orig_w * $scale); $scale = min($max_side / max(1, $orig_w), $max_side / max(1, $orig_h));
$new_h = intval($orig_h * $scale); $preview_w = max(10, intval($orig_w * $scale));
$dst_x = intval(($preview_w - $new_w) / 2); $preview_h = max(10, intval($orig_h * $scale));
$dst_y = intval(($preview_h - $new_h) / 2);
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); 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->config->set('module_img_opti_wm_target_blog', 0);
$this->load->model('module/img_opti'); $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) { if ($res) {
$catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : ''); $catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : '');
$json['success'] = true; $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 { } else {
$json['error'] = $this->language->get('error_wm_preview') ?: 'Failed to generate preview'; $json['error'] = $this->language->get('error_wm_preview') ?: 'Failed to generate preview';
} }
@ -2373,12 +2593,69 @@ class ControllerModuleImgOpti extends Controller {
die('Permission denied'); die('Permission denied');
} }
$this->load->model('setting/setting'); $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( $export = array(
'_module' => 'img_opti', '_module' => 'img_opti',
'_version' => '1.2.0', '_version' => '1.2.0',
'_exported' => date('Y-m-d H:i:s'), '_exported' => date('Y-m-d H:i:s'),
'settings' => $settings 'settings' => $exportSettings
); );
$filename = 'img_opti_settings_' . date('Ymd_His') . '.json'; $filename = 'img_opti_settings_' . date('Ymd_His') . '.json';
$this->response->addHeader('Content-Type: application/json'); $this->response->addHeader('Content-Type: application/json');
@ -2408,13 +2685,16 @@ class ControllerModuleImgOpti extends Controller {
$json['error'] = $this->language->get('error_import_invalid'); $json['error'] = $this->language->get('error_import_invalid');
} else { } else {
$this->load->model('setting/setting'); $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) { foreach ($data['settings'] as $key => $value) {
if (strpos($key, 'module_img_opti') === 0) { 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'); $this->cache->delete('img_opti_disk_stats');
$json['success'] = true; $json['success'] = true;
} }
@ -2422,5 +2702,158 @@ class ControllerModuleImgOpti extends Controller {
$this->response->addHeader('Content-Type: application/json'); $this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($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;
}
} }

View File

@ -3300,8 +3300,10 @@ $(document).ready(function() {
<label class="col-sm-3 control-label"><?php echo $entry_wm_text_color; ?></label> <label class="col-sm-3 control-label"><?php echo $entry_wm_text_color; ?></label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon" id="modal-wm-color-preview" style="background:#ffffff; width:36px;">&nbsp;</span> <span class="input-group-addon" style="padding: 3px 8px; background: #f8f9fa;">
<input type="text" id="wm-rule-text-color" class="form-control" placeholder="#ffffff" maxlength="7"/> <input type="color" id="wm-rule-text-color-picker" value="#ffffff" style="width: 28px; height: 24px; border: none; padding: 0; cursor: pointer; background: transparent; vertical-align: middle;" />
</span>
<input type="text" id="wm-rule-text-color" class="form-control" placeholder="#ffffff" maxlength="7" value="#ffffff" />
</div> </div>
</div> </div>
</div> </div>