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 24a5721..39dff49 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 e5551bf..b2785f7 100644
--- a/upload/admin/controller/module/img_opti.php
+++ b/upload/admin/controller/module/img_opti.php
@@ -2081,8 +2081,10 @@ class ControllerModuleImgOpti extends Controller {
$this->config->set('module_img_opti_wm_corner_radius', isset($this->request->post['module_img_opti_wm_corner_radius']) ? $this->request->post['module_img_opti_wm_corner_radius'] : 0);
$this->config->set('module_img_opti_wm_size_type', isset($this->request->post['module_img_opti_wm_size_type']) ? $this->request->post['module_img_opti_wm_size_type'] : 'original');
$this->config->set('module_img_opti_wm_size_percent', isset($this->request->post['module_img_opti_wm_size_percent']) ? $this->request->post['module_img_opti_wm_size_percent'] : 20);
+ $this->config->set('module_img_opti_wm_size_px', isset($this->request->post['module_img_opti_wm_size_px']) ? $this->request->post['module_img_opti_wm_size_px'] : 200);
$this->config->set('module_img_opti_wm_text_color', isset($this->request->post['module_img_opti_wm_text_color']) ? $this->request->post['module_img_opti_wm_text_color'] : '#ffffff');
$this->config->set('module_img_opti_wm_text_font', isset($this->request->post['module_img_opti_wm_text_font']) ? $this->request->post['module_img_opti_wm_text_font'] : '');
+ $this->config->set('module_img_opti_wm_text_size', isset($this->request->post['module_img_opti_wm_text_size']) ? $this->request->post['module_img_opti_wm_text_size'] : 24);
$this->config->set('module_img_opti_wm_target_product', 1);
$this->config->set('module_img_opti_wm_target_category', 0);
diff --git a/upload/admin/model/module/img_opti.php b/upload/admin/model/module/img_opti.php
index 71fda6a..f49df59 100644
--- a/upload/admin/model/module/img_opti.php
+++ b/upload/admin/model/module/img_opti.php
@@ -4,6 +4,57 @@ if (!defined('DIR_APPLICATION')) { exit; }
class ModelModuleImgOpti extends Model {
private $translations = array();
+ private function paginateScanResult($rows, $data, $headers, $extra = array()) {
+ $start = isset($data['start']) ? max(0, (int)$data['start']) : 0;
+ $limit = isset($data['limit']) ? max(0, (int)$data['limit']) : 0;
+ $total = count($rows);
+
+ if ($limit > 0) {
+ $sliced_rows = array_slice($rows, $start, $limit);
+ } else {
+ $sliced_rows = $rows;
+ }
+
+ $result = array(
+ 'success' => true,
+ 'total' => $total,
+ 'start' => $start,
+ 'limit' => $limit,
+ 'headers' => $headers,
+ 'rows' => $sliced_rows
+ );
+
+ return array_merge($result, $extra);
+ }
+
+ private function formatFixResult($processed = 0, $total = 0) {
+ return array(
+ 'success' => true,
+ 'processed' => (int)$processed,
+ 'total' => (int)$total
+ );
+ }
+
+ public function get_cached_grid($data) {
+ $this->loadLanguageSafe();
+ $tool = isset($data['tool']) ? preg_replace('/[^a-zA-Z0-9_]/', '', (string)$data['tool']) : '';
+ if ($tool === 'broken') $tool = 'broken_db';
+ $cache_file = DIR_CACHE . 'img_opti_' . $tool . '_grid.json';
+ if (!file_exists($cache_file)) {
+ return array('success' => false, 'rows' => array(), 'total' => 0);
+ }
+ $rows = json_decode(file_get_contents($cache_file), true);
+ if (!is_array($rows)) $rows = array();
+
+ return $this->paginateScanResult($rows, $data, array(
+ $this->language->get('text_th_preview'),
+ $this->language->get('text_th_entity'),
+ $this->language->get('text_th_before'),
+ $this->language->get('text_th_after'),
+ $this->language->get('text_th_action')
+ ));
+ }
+
public function backupCatalogTables($tool_name) {
$tables = array(
DB_PREFIX . 'product',
@@ -196,6 +247,157 @@ class ModelModuleImgOpti extends Model {
return $entities;
}
+ public function checkAndFixFakeJpgOnTheFly($filename) {
+ if (!$this->config->get('module_img_opti_status') || !$this->config->get('module_img_opti_auto_fake_jpg_guard')) {
+ return $filename;
+ }
+
+ $cleanFile = str_replace(array('../', '..\\', "\0"), '', (string)$filename);
+ $ext = strtolower(pathinfo($cleanFile, PATHINFO_EXTENSION));
+ if (!in_array($ext, array('jpg', 'jpeg'))) {
+ return $filename;
+ }
+
+ $fullPath = DIR_IMAGE . $cleanFile;
+ if (!is_file($fullPath)) {
+ return $filename;
+ }
+
+ $header = @file_get_contents($fullPath, false, null, 0, 8);
+ if ($header !== "\x89PNG\r\n\x1a\n") {
+ return $filename;
+ }
+
+ $mode = $this->config->get('module_img_opti_fake_jpg_mode');
+ if ($mode === 'png') {
+ $newRelPath = preg_replace('/\.(jpg|jpeg)$/i', '.png', $cleanFile);
+ $newFullPath = DIR_IMAGE . $newRelPath;
+ if (@copy($fullPath, $newFullPath)) {
+ $this->updateImagePathInDb($cleanFile, $newRelPath);
+ $this->deleteImageCache($cleanFile);
+ @unlink($fullPath);
+ return $newRelPath;
+ }
+ } else {
+ if ($this->convertPngToJpgClean($fullPath, 100)) {
+ $this->deleteImageCache($cleanFile);
+ }
+ }
+
+ return $filename;
+ }
+
+ public function getGlobalStats() {
+ $stats = $this->config->get('module_img_opti_global_stats');
+ if (is_string($stats)) {
+ $stats = json_decode($stats, true);
+ }
+ if (!is_array($stats)) {
+ $stats = array(
+ 'total_files' => 0,
+ 'total_bytes_saved' => 0,
+ 'total_original_bytes' => 0,
+ 'last_updated' => 0
+ );
+ }
+
+ $total_files = isset($stats['total_files']) ? (int)$stats['total_files'] : 0;
+ $total_saved = isset($stats['total_bytes_saved']) ? (float)$stats['total_bytes_saved'] : 0.0;
+ $total_orig = isset($stats['total_original_bytes']) ? (float)$stats['total_original_bytes'] : 0.0;
+
+ $mb_saved = round($total_saved / (1024 * 1024), 2);
+ $avg_pct = ($total_orig > 0) ? round(($total_saved / $total_orig) * 100, 1) : 0.0;
+
+ return array(
+ 'total_files' => $total_files,
+ 'total_bytes_saved' => $total_saved,
+ 'total_original_bytes' => $total_orig,
+ 'saved_mb' => $mb_saved,
+ 'avg_pct' => $avg_pct
+ );
+ }
+
+ public function recordGlobalStats($filesCount, $bytesSaved, $originalBytes = 0, $source = 'opti') {
+ if ($filesCount <= 0 && $bytesSaved <= 0) return;
+
+ $stats = $this->config->get('module_img_opti_global_stats');
+ if (is_string($stats)) {
+ $stats = json_decode($stats, true);
+ }
+ if (!is_array($stats)) {
+ $stats = array(
+ 'total_files' => 0,
+ 'total_bytes_saved' => 0,
+ 'total_original_bytes' => 0,
+ 'last_updated' => 0
+ );
+ }
+
+ $stats['total_files'] += (int)$filesCount;
+ $stats['total_bytes_saved'] += (float)$bytesSaved;
+ if ($originalBytes > 0) {
+ $stats['total_original_bytes'] += (float)$originalBytes;
+ } else {
+ $stats['total_original_bytes'] += (float)$bytesSaved;
+ }
+ $stats['last_updated'] = time();
+
+ $this->load->model('setting/setting');
+ $this->model_setting_setting->editSettingValue('module_img_opti', 'module_img_opti_global_stats', json_encode($stats));
+ }
+
+ public function resetGlobalStats() {
+ $stats = array(
+ 'total_files' => 0,
+ 'total_bytes_saved' => 0,
+ 'total_original_bytes' => 0,
+ 'last_updated' => time()
+ );
+ $this->load->model('setting/setting');
+ $this->model_setting_setting->editSettingValue('module_img_opti', 'module_img_opti_global_stats', json_encode($stats));
+ return $this->getGlobalStats();
+ }
+
+ private function updateImagePathInDb($oldPath, $newPath) {
+ $escapedOld = $this->db->escape((string)$oldPath);
+ $escapedNew = $this->db->escape((string)$newPath);
+ $updated = 0;
+
+ $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'");
+ $updated += $this->db->countAffected();
+
+ $this->db->query("UPDATE `" . DB_PREFIX . "product_image` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'");
+ $updated += $this->db->countAffected();
+
+ $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'");
+ $updated += $this->db->countAffected();
+
+ $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'");
+ $updated += $this->db->countAffected();
+
+ $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "banner_image'");
+ if ($table_query->num_rows) {
+ $this->db->query("UPDATE `" . DB_PREFIX . "banner_image` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'");
+ $updated += $this->db->countAffected();
+ }
+
+ $blog_tables = array('oct_blog_article', 'simple_blog_article', 'newsblog_article', 'information');
+ foreach ($blog_tables as $table) {
+ $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . $table . "'");
+ if ($table_query->num_rows) {
+ $column_query = $this->db->query("SHOW COLUMNS FROM `" . DB_PREFIX . $table . "` LIKE 'image'");
+ if ($column_query->num_rows) {
+ try {
+ $this->db->query("UPDATE `" . DB_PREFIX . $table . "` SET image = '" . $escapedNew . "' WHERE image = '" . $escapedOld . "'");
+ $updated += $this->db->countAffected();
+ } catch (Exception $e) {}
+ }
+ }
+ }
+
+ return $updated;
+ }
+
public function scan_broken_db($data = array()) {
$this->loadLanguageSafe();
$rows = array();
@@ -323,20 +525,17 @@ class ModelModuleImgOpti extends Model {
file_put_contents(DIR_CACHE . 'img_opti_broken_db_grid.json', json_encode($rows));
- return array(
- 'success' => true,
- 'headers' => array(
- $this->language->get('text_th_preview'),
- $this->language->get('text_th_entity'),
- $this->language->get('text_th_before'),
- $this->language->get('text_th_after'),
- $this->language->get('text_th_action')
- ),
- 'rows' => $rows,
+ return $this->paginateScanResult($rows, $data, array(
+ $this->language->get('text_th_preview'),
+ $this->language->get('text_th_entity'),
+ $this->language->get('text_th_before'),
+ $this->language->get('text_th_after'),
+ $this->language->get('text_th_action')
+ ), array(
'backup_available' => true,
'quarantine_available' => false,
'description' => $this->language->get('text_description_broken')
- );
+ ));
}
public function fix_broken_db($data) {
@@ -355,6 +554,7 @@ class ModelModuleImgOpti extends Model {
$ph_category = (string)$this->config->get('module_img_opti_ph_category');
$ph_brand = (string)$this->config->get('module_img_opti_ph_brand');
+ $processed = 0;
foreach ($selected as $row_id => $act) {
if ($act === 'ignore') continue;
$parts = explode('-', (string)$row_id, 2);
@@ -393,9 +593,10 @@ class ModelModuleImgOpti extends Model {
$this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $this->db->escape($ph_brand) . "' WHERE manufacturer_id = '" . $id . "'");
}
}
+ $processed++;
}
@unlink(DIR_CACHE . 'img_opti_broken_db_grid.json');
- return array('success' => true);
+ return $this->formatFixResult($processed, count($selected));
}
public function scan_broken_files($data = array()) {
@@ -470,20 +671,17 @@ class ModelModuleImgOpti extends Model {
file_put_contents(DIR_CACHE . 'img_opti_broken_files_grid.json', json_encode($rows));
- return array(
- 'success' => true,
- 'headers' => array(
- $this->language->get('text_th_preview'),
- $this->language->get('text_th_entity'),
- $this->language->get('text_th_before'),
- $this->language->get('text_th_after'),
- $this->language->get('text_th_action')
- ),
- 'rows' => $rows,
+ return $this->paginateScanResult($rows, $data, array(
+ $this->language->get('text_th_preview'),
+ $this->language->get('text_th_entity'),
+ $this->language->get('text_th_before'),
+ $this->language->get('text_th_after'),
+ $this->language->get('text_th_action')
+ ), array(
'backup_available' => false,
'quarantine_available' => true,
'description' => $this->language->get('text_description_broken_files')
- );
+ ));
}
public function fix_broken_files($data) {
@@ -819,25 +1017,14 @@ class ModelModuleImgOpti extends Model {
if (!$jpgDir || strpos(str_replace('\\', '/', $jpgDir), str_replace('\\', '/', $realImageDir)) !== 0) continue;
$relJpg = ltrim(str_replace(str_replace('\\', '/', $realImageDir), '', str_replace('\\', '/', $jpgPathStr)), '/');
- $img = @imagecreatefrompng($fullPath);
- if ($img) {
- if (function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) {
- imagepalettetotruecolor($img);
- }
- $bg = imagecreatetruecolor(imagesx($img), imagesy($img));
- imagefill($bg, 0, 0, imagecolorallocate($bg, 255, 255, 255));
- imagecopy($bg, $img, 0, 0, 0, 0, imagesx($img), imagesy($img));
- if (imagejpeg($bg, $jpgPathStr, 85)) {
- $dbOld = $this->db->escape($pngClean);
- $dbNew = $this->db->escape($relJpg);
- $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'");
- $this->db->query("UPDATE `" . DB_PREFIX . "product_image` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'");
- $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'");
- $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'");
- $this->quarantineFile($pngClean, 'png_jpg');
- }
- imagedestroy($img);
- imagedestroy($bg);
+ if ($this->convertPngToJpgClean($fullPath, $jpgPathStr, 85)) {
+ $dbOld = $this->db->escape($pngClean);
+ $dbNew = $this->db->escape($relJpg);
+ $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'");
+ $this->db->query("UPDATE `" . DB_PREFIX . "product_image` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'");
+ $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'");
+ $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'");
+ $this->quarantineFile($pngClean, 'png_jpg');
}
}
@unlink(DIR_CACHE . 'img_opti_png_jpg_grid.json');
@@ -1738,7 +1925,9 @@ class ModelModuleImgOpti extends Model {
public function applyDynamicWatermark($cached_file, $original_file, $is_preview = false) {
$realCachedFile = realpath($cached_file);
$realImageDir = realpath(DIR_IMAGE);
- if (!$realCachedFile || strpos(str_replace('\\', '/', $realCachedFile), str_replace('\\', '/', $realImageDir)) !== 0) return false;
+ $cleanCached = mb_strtolower(str_replace('\\', '/', $realCachedFile ?: ''), 'UTF-8');
+ $cleanImageDir = mb_strtolower(str_replace('\\', '/', $realImageDir ?: ''), 'UTF-8');
+ if (!$realCachedFile || strpos($cleanCached, $cleanImageDir) !== 0) return false;
$escaped_original = $this->db->escape((string)$original_file);
$matched_rules = array();
@@ -1755,8 +1944,10 @@ class ModelModuleImgOpti extends Model {
'corner_radius' => $this->config->get('module_img_opti_wm_corner_radius'),
'size_type' => $this->config->get('module_img_opti_wm_size_type'),
'size_percent' => $this->config->get('module_img_opti_wm_size_percent'),
+ 'size_px' => $this->config->get('module_img_opti_wm_size_px'),
'text_color' => $this->config->get('module_img_opti_wm_text_color'),
- 'text_font' => $this->config->get('module_img_opti_wm_text_font')
+ 'text_font' => $this->config->get('module_img_opti_wm_text_font'),
+ 'text_size' => $this->config->get('module_img_opti_wm_text_size')
);
} else {
$rules_json = $this->config->get('module_img_opti_wm_rules');
@@ -1940,7 +2131,8 @@ class ModelModuleImgOpti extends Model {
$type = preg_replace('/[^a-z]/', '', $rule['type']);
$imagePathStr = str_replace(array('../', '..\\', ' '), '', (string)$rule['image']);
$imagePath = realpath(DIR_IMAGE . $imagePathStr);
- if ($imagePath && strpos(str_replace('\\', '/', $imagePath), str_replace('\\', '/', $realImageDir)) !== 0) {
+ $cleanImagePath = mb_strtolower(str_replace('\\', '/', $imagePath ?: ''), 'UTF-8');
+ if ($imagePath && strpos($cleanImagePath, $cleanImageDir) !== 0) {
$imagePath = false;
}
$textVal = (string)$rule['text_val'];
@@ -1948,6 +2140,8 @@ class ModelModuleImgOpti extends Model {
$opacity = (int)$rule['opacity'];
$size_type = isset($rule['size_type']) ? $rule['size_type'] : 'original';
$size_percent = isset($rule['size_percent']) ? (int)$rule['size_percent'] : 20;
+ $size_px = isset($rule['size_px']) ? (int)$rule['size_px'] : 200;
+ $text_size = isset($rule['text_size']) ? (int)$rule['text_size'] : 24;
$angle = isset($rule['angle']) ? (int)$rule['angle'] : 0;
$corner_radius = isset($rule['corner_radius']) ? (int)$rule['corner_radius'] : 0;
$text_font = isset($rule['text_font']) ? (string)$rule['text_font'] : '';
@@ -1980,7 +2174,13 @@ class ModelModuleImgOpti extends Model {
}
}
- if ($size_type === 'percent' && $size_percent > 0) {
+ if ($size_type === 'px' && $size_px > 0) {
+ $targetW = min($imgW, $size_px);
+ $targetH = intval(($wm->getImageHeight() / $wm->getImageWidth()) * $targetW);
+ if ($targetW > 0 && $targetH > 0) {
+ $wm->resizeImage($targetW, $targetH, Imagick::FILTER_LANCZOS, 1);
+ }
+ } elseif ($size_type === 'percent' && $size_percent > 0) {
$targetW = intval(($imgW * $size_percent) / 100);
$targetH = intval(($wm->getImageHeight() / $wm->getImageWidth()) * $targetW);
if ($targetW > 0 && $targetH > 0) {
@@ -2016,7 +2216,7 @@ class ModelModuleImgOpti extends Model {
if (is_file($fontFile)) {
$draw->setFont($fontFile);
}
- $fontSize = max(12, $imgW * 0.04);
+ $fontSize = ($text_size > 0) ? $text_size : 24;
$draw->setFontSize($fontSize);
$colorHex = $text_color;
list($r, $g, $b) = $this->hex2rgb($colorHex);
@@ -2070,7 +2270,8 @@ class ModelModuleImgOpti extends Model {
$type = preg_replace('/[^a-z]/', '', $rule['type']);
$imagePathStr = str_replace(array('../', '..\\', ' '), '', (string)$rule['image']);
$imagePath = realpath(DIR_IMAGE . $imagePathStr);
- if ($imagePath && strpos(str_replace('\\', '/', $imagePath), str_replace('\\', '/', $realImageDir)) !== 0) {
+ $cleanImagePath = mb_strtolower(str_replace('\\', '/', $imagePath ?: ''), 'UTF-8');
+ if ($imagePath && strpos($cleanImagePath, $cleanImageDir) !== 0) {
$imagePath = false;
}
$textVal = (string)$rule['text_val'];
@@ -2078,6 +2279,8 @@ class ModelModuleImgOpti extends Model {
$opacity = (int)$rule['opacity'];
$size_type = isset($rule['size_type']) ? $rule['size_type'] : 'original';
$size_percent = isset($rule['size_percent']) ? (int)$rule['size_percent'] : 20;
+ $size_px = isset($rule['size_px']) ? (int)$rule['size_px'] : 200;
+ $text_size = isset($rule['text_size']) ? (int)$rule['text_size'] : 24;
$angle = isset($rule['angle']) ? (int)$rule['angle'] : 0;
$corner_radius = isset($rule['corner_radius']) ? (int)$rule['corner_radius'] : 0;
$text_font = isset($rule['text_font']) ? (string)$rule['text_font'] : '';
@@ -2090,6 +2293,12 @@ class ModelModuleImgOpti extends Model {
if ($type === 'image') {
$wm = @imagecreatefrompng($imagePath);
+ if (!$wm) {
+ $wm = @imagecreatefromjpeg($imagePath);
+ }
+ if (!$wm && function_exists('imagecreatefromwebp')) {
+ $wm = @imagecreatefromwebp($imagePath);
+ }
if ($wm) {
if (function_exists('imagepalettetotruecolor') && !imageistruecolor($wm)) {
imagepalettetotruecolor($wm);
@@ -2122,7 +2331,22 @@ class ModelModuleImgOpti extends Model {
$wmH = $new_wm_h;
}
}
- if ($size_type === 'percent' && $size_percent > 0) {
+ if ($size_type === 'px' && $size_px > 0) {
+ $targetW = min($imgW, $size_px);
+ $targetH = intval(($wmH / $wmW) * $targetW);
+ if ($targetW > 0 && $targetH > 0) {
+ $resized_wm = imagecreatetruecolor($targetW, $targetH);
+ imagealphablending($resized_wm, false);
+ imagesavealpha($resized_wm, true);
+ $transparent = imagecolorallocatealpha($resized_wm, 255, 255, 255, 127);
+ imagefill($resized_wm, 0, 0, $transparent);
+ imagecopyresampled($resized_wm, $wm, 0, 0, 0, 0, $targetW, $targetH, $wmW, $wmH);
+ imagedestroy($wm);
+ $wm = $resized_wm;
+ $wmW = $targetW;
+ $wmH = $targetH;
+ }
+ } elseif ($size_type === 'percent' && $size_percent > 0) {
$targetW = intval(($imgW * $size_percent) / 100);
$targetH = intval(($wmH / $wmW) * $targetW);
if ($targetW > 0 && $targetH > 0) {
@@ -2211,10 +2435,18 @@ class ModelModuleImgOpti extends Model {
}
}
+ $userFontSize = ($text_size > 0) ? $text_size : 24;
+
if (!is_file($fontFile)) {
- $fontSize = max(3, intval($imgW / 50));
- $fontWidth = imagefontwidth($fontSize) * strlen($textVal);
- $fontHeight = imagefontheight($fontSize);
+ $gdFont = 3;
+ if ($userFontSize <= 14) $gdFont = 1;
+ elseif ($userFontSize <= 20) $gdFont = 2;
+ elseif ($userFontSize <= 28) $gdFont = 3;
+ elseif ($userFontSize <= 38) $gdFont = 4;
+ else $gdFont = 5;
+
+ $fontWidth = imagefontwidth($gdFont) * strlen($textVal);
+ $fontHeight = imagefontheight($gdFont);
$posX = 0; $posY = 0; $pad = 10;
if (in_array($position, array(1, 4, 7))) $posX = $pad;
elseif (in_array($position, array(2, 5, 8))) $posX = ($imgW - $fontWidth) / 2;
@@ -2226,9 +2458,9 @@ class ModelModuleImgOpti extends Model {
$alpha = intval(127 - (127 * ($opacity / 100)));
$color = imagecolorallocatealpha($img, 255, 255, 255, $alpha);
- imagestring($img, $fontSize, $posX, $posY, $textVal, $color);
+ imagestring($img, $gdFont, $posX, $posY, $textVal, $color);
} else {
- $fontSize = max(10, intval($imgW / 25));
+ $fontSize = $userFontSize;
$colorHex = $text_color;
list($r, $g, $b) = $this->hex2rgb($colorHex);
$alpha = intval(127 - (127 * ($opacity / 100)));
@@ -2497,9 +2729,19 @@ private function roundCornersGD($im, $radius) {
'before' => $this->language->get('text_grid_missing_html') . ': ' . htmlspecialchars($cleanRelPath, ENT_QUOTES, 'UTF-8'),
'after' => $this->language->get('text_grid_html_after'),
'actions' => array(
+ 'remove' => $this->translate('text_action_remove_tag', 'Remove Tag'),
+ 'placeholder' => $this->language->get('text_action_placeholder'),
'ignore' => $this->language->get('text_action_ignore')
),
- 'selected_action' => 'ignore'
+ 'selected_action' => 'remove',
+ 'meta' => array(
+ 'table' => $table,
+ 'id_field' => $cfg['id'],
+ 'id' => (int)$row[$cfg['id']],
+ 'field' => $cfg['field'],
+ 'src' => $src,
+ 'rel_path' => $cleanRelPath
+ )
);
}
}
@@ -2508,6 +2750,8 @@ private function roundCornersGD($im, $radius) {
}
}
+ file_put_contents(DIR_CACHE . 'img_opti_html_broken_grid.json', json_encode($rows));
+
return array(
'success' => true,
'headers' => array(
@@ -2524,6 +2768,57 @@ private function roundCornersGD($im, $radius) {
);
}
+ public function fix_html_broken($data) {
+ $selected = isset($data['selected']) ? (array)$data['selected'] : array();
+ $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0;
+
+ $cache_path = DIR_CACHE . 'img_opti_html_broken_grid.json';
+ if (!file_exists($cache_path)) {
+ return array('success' => true);
+ }
+
+ $grid = json_decode(file_get_contents($cache_path), true);
+ if (!$grid) return array('success' => true);
+
+ $rows_map = array();
+ foreach ($grid as $row) {
+ $rows_map[$row['id']] = $row;
+ if ($fix_all === 1 || empty($selected)) {
+ $selected[$row['id']] = $row['selected_action'];
+ }
+ }
+
+ $ph_product = (string)$this->config->get('module_img_opti_ph_product');
+ if (!$ph_product) $ph_product = 'no_image.png';
+
+ foreach ($selected as $row_id => $act) {
+ if ($act === 'ignore' || !isset($rows_map[$row_id])) continue;
+ $row = $rows_map[$row_id];
+ $meta = $row['meta'];
+ $table = preg_replace('/[^a-zA-Z0-9_]/', '', $meta['table']);
+ $id_field = preg_replace('/[^a-zA-Z0-9_]/', '', $meta['id_field']);
+ $field = preg_replace('/[^a-zA-Z0-9_]/', '', $meta['field']);
+ $id = (int)$meta['id'];
+ $src = $meta['src'];
+
+ $query = $this->db->query("SELECT `" . $field . "` FROM `" . DB_PREFIX . $table . "` WHERE `" . $id_field . "` = '" . $id . "'");
+ if ($query->num_rows) {
+ $desc = html_entity_decode($query->row[$field], ENT_QUOTES, 'UTF-8');
+ if ($act === 'remove') {
+ $desc = preg_replace('/
]+src=(?:"|\')' . preg_quote($src, '/') . '(?:"|\')[^>]*>/i', '', $desc);
+ } elseif ($act === 'placeholder') {
+ $catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : '');
+ $new_src = $catalog_url . 'image/' . $ph_product;
+ $desc = str_replace($src, $new_src, $desc);
+ }
+ $this->db->query("UPDATE `" . DB_PREFIX . $table . "` SET `" . $field . "` = '" . $this->db->escape($desc) . "' WHERE `" . $id_field . "` = '" . $id . "'");
+ }
+ }
+
+ @unlink($cache_path);
+ return array('success' => true);
+ }
+
public function scan_folder_tree($data) {
$this->loadLanguageSafe();
$dir = realpath(DIR_IMAGE . 'catalog');
@@ -3231,8 +3526,8 @@ private function roundCornersGD($im, $radius) {
private function loadLanguageSafe() {
$data = array();
$en_paths = array(
- DIR_LANGUAGE . 'en-gb/module/img_opti.php',
- DIR_LANGUAGE . 'english/module/img_opti.php'
+ DIR_LANGUAGE . 'en-gb/extension/module/img_opti.php',
+ DIR_LANGUAGE . 'english/extension/module/img_opti.php'
);
foreach ($en_paths as $path) {
if (is_file($path)) {
@@ -3255,8 +3550,8 @@ private function roundCornersGD($im, $radius) {
$lang_lower = strtolower((string)$lang);
if ($lang_lower && $lang_lower !== 'en-gb' && $lang_lower !== 'english') {
$paths = array(
- DIR_LANGUAGE . $lang . '/module/img_opti.php',
- DIR_LANGUAGE . $lang_lower . '/module/img_opti.php'
+ DIR_LANGUAGE . $lang . '/extension/module/img_opti.php',
+ DIR_LANGUAGE . $lang_lower . '/extension/module/img_opti.php'
);
if (isset($this->db)) {
@@ -3265,25 +3560,25 @@ private function roundCornersGD($im, $radius) {
$query_lang = $this->db->query("SELECT directory FROM `" . DB_PREFIX . "language` WHERE code = '" . $escaped_lang . "' OR directory = '" . $escaped_lang . "' LIMIT 1");
if ($query_lang && $query_lang->num_rows && !empty($query_lang->row['directory'])) {
$db_dir = $query_lang->row['directory'];
- $paths[] = DIR_LANGUAGE . $db_dir . '/module/img_opti.php';
- $paths[] = DIR_LANGUAGE . strtolower($db_dir) . '/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . $db_dir . '/extension/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . strtolower($db_dir) . '/extension/module/img_opti.php';
}
} catch (Exception $e) {}
}
if (strpos($lang_lower, 'ru') !== false || strpos($lang_lower, 'rus') !== false) {
- $paths[] = DIR_LANGUAGE . 'ru-ru/module/img_opti.php';
- $paths[] = DIR_LANGUAGE . 'russian/module/img_opti.php';
- $paths[] = DIR_LANGUAGE . 'ru-RU/module/img_opti.php';
- $paths[] = DIR_LANGUAGE . 'ru/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . 'ru-ru/extension/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . 'russian/extension/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . 'ru-RU/extension/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . 'ru/extension/module/img_opti.php';
} elseif (strpos($lang_lower, 'uk') !== false || strpos($lang_lower, 'ua') !== false || strpos($lang_lower, 'ukr') !== false) {
- $paths[] = DIR_LANGUAGE . 'uk-ua/module/img_opti.php';
- $paths[] = DIR_LANGUAGE . 'ukrainian/module/img_opti.php';
- $paths[] = DIR_LANGUAGE . 'uk-UA/module/img_opti.php';
- $paths[] = DIR_LANGUAGE . 'uk/module/img_opti.php';
- $paths[] = DIR_LANGUAGE . 'ua/module/img_opti.php';
- $paths[] = DIR_LANGUAGE . 'uk_ua/module/img_opti.php';
- $paths[] = DIR_LANGUAGE . 'uk_UA/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . 'uk-ua/extension/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . 'ukrainian/extension/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . 'uk-UA/extension/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . 'uk/extension/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . 'ua/extension/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . 'uk_ua/extension/module/img_opti.php';
+ $paths[] = DIR_LANGUAGE . 'uk_UA/extension/module/img_opti.php';
}
foreach ($paths as $path) {
if (is_file($path)) {
@@ -3415,16 +3710,8 @@ private function roundCornersGD($im, $radius) {
$fullPath = realpath(DIR_IMAGE . $relPath);
if (!$fullPath || strpos(str_replace('\\', '/', $fullPath), str_replace('\\', '/', $realImageDir)) !== 0 || !is_file($fullPath)) continue;
- if (extension_loaded('imagick')) {
- try {
- $img = new Imagick($fullPath);
- if ($img->getImageColorspace() == Imagick::COLORSPACE_CMYK) {
- $img->transformImageColorspace(Imagick::COLORSPACE_SRGB);
- $img->writeImage($fullPath);
- $this->deleteImageCache($relPath);
- }
- $img->clear(); $img->destroy();
- } catch (Exception $e) {}
+ if ($this->normalizeCmykToRgbJpeg($fullPath, 90)) {
+ $this->deleteImageCache($relPath);
}
}
@unlink(DIR_CACHE . 'img_opti_cmyk_grid.json');
@@ -3573,24 +3860,26 @@ private function roundCornersGD($im, $radius) {
private function deleteImageCache($relPath) {
$info = $this->mb_pathinfo($relPath);
- if (!isset($info['extension']) || !isset($info['dirname'])) return;
- $dir = DIR_IMAGE . 'cache/' . $info['dirname'];
- if (!is_dir($dir)) return;
+ if (!isset($info['filename'])) return;
$filename = $info['filename'];
- $ext = $info['extension'];
-
- $files = glob($dir . '/' . $filename . '-*x*.' . $ext);
- if ($files) {
- foreach ($files as $file) {
- @unlink($file);
+ $cache_dir = realpath(DIR_IMAGE . 'cache');
+ if (!$cache_dir || !is_dir($cache_dir)) return;
+
+ try {
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($cache_dir, RecursiveDirectoryIterator::SKIP_DOTS),
+ RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach ($iterator as $file) {
+ if ($file->isFile()) {
+ $fn = $file->getFilename();
+ if (strpos($fn, $filename . '-') === 0 || strpos($fn, $filename . '.') === 0 || strpos($fn, $filename . '_') === 0) {
+ @unlink($file->getPathname());
+ }
+ }
}
- }
- $files_webp = glob($dir . '/' . $filename . '-*x*.webp');
- if ($files_webp) {
- foreach ($files_webp as $file) {
- @unlink($file);
- }
- }
+ } catch (\Exception $e) {}
}
private function mb_pathinfo($path, $options = null) {
@@ -3662,4 +3951,1084 @@ private function roundCornersGD($im, $radius) {
$rel = ltrim($rel, '/');
return $this->safe_utf8($rel);
}
+
+ 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') {
+ if (extension_loaded('imagick')) {
+ try {
+ $im = new \Imagick($sourcePngPath);
+ $bg = new \Imagick();
+ $fillColor = new \ImagickPixel($bgColorHex);
+ $bg->newImage($im->getImageWidth(), $im->getImageHeight(), $fillColor);
+ $bg->compositeImage($im, \Imagick::COMPOSITE_OVER, 0, 0);
+ $bg->setImageFormat('jpg');
+ $bg->setImageCompressionQuality($quality);
+ $bg->writeImage($targetJpgPath);
+ $bg->clear(); $bg->destroy();
+ $im->clear(); $im->destroy();
+ return true;
+ } catch (\Exception $e) {}
+ }
+
+ $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;
+ }
+
+ public function sanitizeSvgFile($filePath) {
+ if (!is_file($filePath)) {
+ return false;
+ }
+ $content = @file_get_contents($filePath);
+ if ($content === false) {
+ return false;
+ }
+ $clean = $this->sanitizeSvgContent($content);
+ return (@file_put_contents($filePath, $clean) !== false);
+ }
+
+ public function sanitizeSvgContent($content) {
+ if (empty($content)) {
+ return $content;
+ }
+
+ $content = preg_replace('/]*\[.*?\]>/si', '', $content);
+ $content = preg_replace('/]*>/i', '', $content);
+
+ $previous_libxml_state = false;
+ if (PHP_VERSION_ID < 80000 && function_exists('libxml_disable_entity_loader')) {
+ $previous_libxml_state = @libxml_disable_entity_loader(true);
+ }
+
+ $dom = new \DOMDocument();
+ $dom->formatOutput = true;
+ $libxml_flags = LIBXML_NONET | LIBXML_NOWARNING | LIBXML_NOERROR;
+ if (defined('LIBXML_NOENT')) {
+ $libxml_flags |= LIBXML_NOENT;
+ }
+
+ $loaded = @$dom->loadXML($content, $libxml_flags);
+
+ if ($previous_libxml_state !== false && PHP_VERSION_ID < 80000 && function_exists('libxml_disable_entity_loader')) {
+ @libxml_disable_entity_loader($previous_libxml_state);
+ }
+
+ if ($loaded) {
+ $disallowed_tags = array('script', 'foreignobject', 'object', 'embed', 'iframe', 'applet', 'meta', 'link');
+ foreach ($disallowed_tags as $tag) {
+ $nodes = $dom->getElementsByTagName($tag);
+ while ($nodes->length > 0) {
+ $item = $nodes->item(0);
+ if ($item && $item->parentNode) {
+ $item->parentNode->removeChild($item);
+ }
+ }
+ }
+
+ $xpath = new \DOMXPath($dom);
+
+ $nodes = $xpath->query('//@*[starts-with(name(), "on") or starts-with(name(), "ON")]');
+ if ($nodes) {
+ $attrs_to_remove = array();
+ foreach ($nodes as $attr) {
+ $attrs_to_remove[] = $attr;
+ }
+ foreach ($attrs_to_remove as $attr) {
+ if ($attr->ownerElement) {
+ $attr->ownerElement->removeAttribute($attr->name);
+ }
+ }
+ }
+
+ $nodes = $xpath->query('//@*[name()="href" or name()="xlink:href" or name()="src" or name()="action" or name()="formaction" or name()="data" or name()="values"]');
+ if ($nodes) {
+ foreach ($nodes as $attr) {
+ $val = strtolower(trim($attr->value));
+ if (strpos($val, 'javascript:') !== false || strpos($val, 'data:text/html') !== false || strpos($val, 'data:application/javascript') !== false) {
+ if ($attr->ownerElement) {
+ $attr->ownerElement->removeAttribute($attr->name);
+ }
+ }
+ }
+ }
+
+ $styles = $dom->getElementsByTagName('style');
+ foreach ($styles as $styleNode) {
+ $styleText = $styleNode->nodeValue;
+ if ($styleText) {
+ $styleText = preg_replace('/expression\s*\(/i', 'blocked(', $styleText);
+ $styleText = preg_replace('/url\s*\(\s*["\']?\s*javascript:[^"\']*\s*["\']?\s*\)/i', 'none', $styleText);
+ $styleText = preg_replace('/url\s*\(\s*["\']?\s*data:text\/html[^"\']*\s*["\']?\s*\)/i', 'none', $styleText);
+ $styleNode->nodeValue = $styleText;
+ }
+ }
+
+ $clean_content = $dom->saveXML();
+ if ($clean_content !== false) {
+ return $clean_content;
+ }
+ }
+
+ $content = preg_replace('/