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('/]*>(.*?)<\/script>/is', '', $content); + $content = preg_replace('/\bon\w+\s*=\s*(?:"[^"]*"|\'[^\']*\')/i', '', $content); + $content = preg_replace('/(?:href|xlink:href|src)\s*=\s*["\']\s*javascript:[^"\']*["\']/i', '', $content); + $content = preg_replace('/<\s*(?:foreignObject|object|embed|iframe|applet)\b[^>]*>.*?<\/\s*\1\s*>/is', '', $content); + $content = preg_replace('/<\s*(?:foreignObject|object|embed|iframe|applet)\b[^>]*\/?\s*>/i', '', $content); + $content = preg_replace('/expression\s*\(/i', 'blocked(', $content); + + return $content; + } + + public function detectSvgThreats($content) { + $threats = array(); + if (empty($content)) return $threats; + + if (preg_match('/]*\[.*?\]>/si', $content) || preg_match('/]*>/i', $content)) { + $threats[] = 'script_tag'; + } + if (preg_match('/\bon[a-z]+\s*=/i', $content)) { + $threats[] = 'event_handler'; + } + if (preg_match('/(?:href|xlink:href|src|action|data)\s*=\s*["\']\s*javascript:/i', $content)) { + $threats[] = 'javascript_uri'; + } + if (preg_match('/<\s*foreignObject\b/i', $content)) { + $threats[] = 'foreign_object'; + } + if (preg_match('/<\s*(?:object|embed|iframe|applet)\b/i', $content)) { + $threats[] = 'embed_object'; + } + if (preg_match('/expression\s*\(|url\s*\(\s*["\']?\s*javascript:/i', $content)) { + $threats[] = 'css_injection'; + } + return $threats; + } + + public function scan_svg_security($data = array()) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $limit = isset($data['module_img_opti_svg_security_limit']) ? (int)$data['module_img_opti_svg_security_limit'] : 100; + if ($limit <= 0) $limit = 100; + + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)); + + foreach ($iterator as $file) { + if ($file->isFile()) { + $realPath = realpath($file->getPathname()); + if ($realPath && strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) === 0) { + $ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION)); + if ($ext === 'svg') { + $content = @file_get_contents($realPath); + if ($content !== false) { + $threats = $this->detectSvgThreats($content); + if (!empty($threats)) { + $relPath = $this->getRelativeImagePath($realPath); + $filename = $this->mb_basename($relPath); + + $threat_badges = array(); + foreach ($threats as $t) { + $lang_key = 'text_svg_threat_' . $t; + $label = $this->language->get($lang_key); + if ($label === $lang_key) $label = $t; + $threat_badges[] = '' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ''; + } + + $preview_url = '../image/' . str_replace('\\', '/', $relPath); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
', $entities) : '' . $this->language->get('text_no_relations') . ''; + + $rows[] = array( + 'id' => 'svg_security-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
' . $entities_html, + 'before' => '' . sprintf($this->language->get('text_svg_threats_found'), count($threats)) . '
' . implode(' ', $threat_badges), + 'after' => ' ' . $this->language->get('text_svg_clean_success') . '', + 'actions' => array( + 'sanitize' => $this->language->get('text_action_apply_changes'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'sanitize', + 'meta' => array( + 'path' => $relPath + ) + ); + + if (count($rows) >= $limit) { + break; + } + } + } + } + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_svg_security_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, + 'backup_available' => true, + 'quarantine_available' => true, + 'description' => $this->language->get('text_description_svg_security') + ); + } + + public function fix_svg_security($data) { + $this->backupCatalogTables('svg_security'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $cache_file = DIR_CACHE . 'img_opti_svg_security_grid.json'; + if (!is_file($cache_file)) { + return $this->formatFixResult(0, 0); + } + + $grid = json_decode(file_get_contents($cache_file), true); + if (!is_array($grid)) { + return $this->formatFixResult(0, 0); + } + + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $realImageDir = realpath(DIR_IMAGE); + $processed = 0; + + foreach ($selected as $row_id => $act) { + if ($act !== 'sanitize' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $relPath = (string)$row['meta']['path']; + $cleanBadFile = str_replace(array('../', '..\\', "\0"), '', $relPath); + $fullPath = DIR_IMAGE . $cleanBadFile; + + if (!is_file($fullPath)) continue; + $realFullPath = realpath($fullPath); + if (!$realFullPath || strpos(str_replace('\\', '/', $realFullPath), str_replace('\\', '/', $realImageDir)) !== 0) continue; + + $svgContent = @file_get_contents($fullPath); + if ($svgContent === false) continue; + + $this->quarantineFile($relPath, 'svg_security'); + + $cleanSvg = $this->sanitizeSvgContent($svgContent); + if (@file_put_contents($fullPath, $cleanSvg) !== false) { + $processed++; + } + } + + return $this->formatFixResult($processed, count($selected)); + } + + public function replaceBlackBackground($img, $bgColorHex = '#FFFFFF', $darkness = 30) { + return $this->replaceBlackBackgroundAdaptive($img, $bgColorHex, $darkness, 'edges_only'); + } + + public function replaceBlackBackgroundAdaptive($img, $bgColorHex = '#FFFFFF', $darkness = 30, $mode = 'edges_only') { + if (!$img) return $img; + + $width = imagesx($img); + $height = imagesy($img); + if ($width <= 0 || $height <= 0) return $img; + + $r_target = 255; $g_target = 255; $b_target = 255; + if (preg_match('/^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i', $bgColorHex, $m)) { + $r_target = hexdec($m[1]); + $g_target = hexdec($m[2]); + $b_target = hexdec($m[3]); + } + + $sample_points = array(); + for ($i = 0; $i < 20; $i++) { + $sample_points[] = array((int)($i * ($width - 1) / 19), 0); + $sample_points[] = array((int)($i * ($width - 1) / 19), $height - 1); + $sample_points[] = array(0, (int)($i * ($height - 1) / 19)); + $sample_points[] = array($width - 1, (int)($i * ($height - 1) / 19)); + } + + $sum_r = 0; $sum_g = 0; $sum_b = 0; $bg_samples = 0; + foreach ($sample_points as $p) { + $rgb = imagecolorat($img, $p[0], $p[1]); + $alpha = ($rgb & 0x7F000000) >> 24; + if ($alpha <= 10) { + $r = ($rgb >> 16) & 0xFF; + $g = ($rgb >> 8) & 0xFF; + $b = $rgb & 0xFF; + if ($r <= $darkness + 35 && $g <= $darkness + 35 && $b <= $darkness + 35) { + $sum_r += $r; + $sum_g += $g; + $sum_b += $b; + $bg_samples++; + } + } + } + + $base_r = ($bg_samples > 0) ? (int)($sum_r / $bg_samples) : 0; + $base_g = ($bg_samples > 0) ? (int)($sum_g / $bg_samples) : 0; + $base_b = ($bg_samples > 0) ? (int)($sum_b / $bg_samples) : 0; + + $tolerance_inner = ($mode === 'all_smooth') ? max(45.0, (float)$darkness * 1.3) : max(30.0, (float)$darkness * 0.9); + $tolerance_outer = ($mode === 'all_smooth') ? ($tolerance_inner + 45.0) : ($tolerance_inner + 25.0); // Feathering range for anti-aliasing + + $fillColor = imagecolorallocate($img, $r_target, $g_target, $b_target); + + if ($mode === 'all' || $mode === 'all_smooth') { + for ($y = 0; $y < $height; $y++) { + for ($x = 0; $x < $width; $x++) { + $rgb = imagecolorat($img, $x, $y); + $alpha = ($rgb & 0x7F000000) >> 24; + if ($alpha > 10) continue; + + $r = ($rgb >> 16) & 0xFF; + $g = ($rgb >> 8) & 0xFF; + $b = $rgb & 0xFF; + + $dist = sqrt(pow($r - $base_r, 2) + pow($g - $base_g, 2) + pow($b - $base_b, 2)); + + if ($dist <= $tolerance_inner) { + imagesetpixel($img, $x, $y, $fillColor); + } elseif ($dist <= $tolerance_outer) { + $blendFactor = ($dist - $tolerance_inner) / ($tolerance_outer - $tolerance_inner); + $r_new = (int)($r * $blendFactor + $r_target * (1.0 - $blendFactor)); + $g_new = (int)($g * $blendFactor + $g_target * (1.0 - $blendFactor)); + $b_new = (int)($b * $blendFactor + $b_target * (1.0 - $blendFactor)); + $blendedColor = imagecolorallocate($img, $r_new, $g_new, $b_new); + imagesetpixel($img, $x, $y, $blendedColor); + } + } + } + } else { + $seeds = array(); + for ($x = 0; $x < $width; $x += 10) { + $seeds[] = array($x, 0); + $seeds[] = array($x, $height - 1); + } + for ($y = 0; $y < $height; $y += 10) { + $seeds[] = array(0, $y); + $seeds[] = array($width - 1, $y); + } + $seeds[] = array($width - 1, $height - 1); + + $queue = array(); + $visited = array_fill(0, $height, array_fill(0, $width, false)); + + foreach ($seeds as $c) { + $x = $c[0]; $y = $c[1]; + $rgb = imagecolorat($img, $x, $y); + $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; + $dist = sqrt(pow($r - $base_r, 2) + pow($g - $base_g, 2) + pow($b - $base_b, 2)); + if ($dist <= $tolerance_outer && !$visited[$y][$x]) { + $queue[] = array($x, $y); + $visited[$y][$x] = true; + } + } + + while (!empty($queue)) { + $curr = array_pop($queue); + $cx = $curr[0]; $cy = $curr[1]; + + $rgb = imagecolorat($img, $cx, $cy); + $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; + $dist = sqrt(pow($r - $base_r, 2) + pow($g - $base_g, 2) + pow($b - $base_b, 2)); + + if ($dist <= $tolerance_inner) { + imagesetpixel($img, $cx, $cy, $fillColor); + } else { + $blendFactor = ($dist - $tolerance_inner) / ($tolerance_outer - $tolerance_inner); + $r_new = (int)($r * $blendFactor + $r_target * (1.0 - $blendFactor)); + $g_new = (int)($g * $blendFactor + $g_target * (1.0 - $blendFactor)); + $b_new = (int)($b * $blendFactor + $b_target * (1.0 - $blendFactor)); + $blendedColor = imagecolorallocate($img, $r_new, $g_new, $b_new); + imagesetpixel($img, $cx, $cy, $blendedColor); + } + + $neighbors = array( + array($cx + 1, $cy), array($cx - 1, $cy), + array($cx, $cy + 1), array($cx, $cy - 1) + ); + + foreach ($neighbors as $n) { + $nx = $n[0]; $ny = $n[1]; + if ($nx >= 0 && $nx < $width && $ny >= 0 && $ny < $height && !$visited[$ny][$nx]) { + $visited[$ny][$nx] = true; + $nrgb = imagecolorat($img, $nx, $ny); + $nr = ($nrgb >> 16) & 0xFF; $ng = ($nrgb >> 8) & 0xFF; $nb = $nrgb & 0xFF; + $ndist = sqrt(pow($nr - $base_r, 2) + pow($ng - $base_g, 2) + pow($nb - $base_b, 2)); + if ($ndist <= $tolerance_outer) { + $queue[] = array($nx, $ny); + } + } + } + } + } + + return $img; + } + + public function replaceBlackBackgroundImagick($filePath, $bgColorHex = '#FFFFFF', $darkness = 30, $mode = 'all_smooth') { + if (!extension_loaded('imagick') || !is_file($filePath)) { + return false; + } + + try { + $im = new \Imagick($filePath); + + $r_target = 255; $g_target = 255; $b_target = 255; + if (preg_match('/^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i', $bgColorHex, $m)) { + $r_target = hexdec($m[1]); + $g_target = hexdec($m[2]); + $b_target = hexdec($m[3]); + } + + $fillColor = new \ImagickPixel("rgb($r_target, $g_target, $b_target)"); + $fuzz = (\Imagick::getQuantum() * (max(30, $darkness * 1.3) / 255)); + + if ($mode === 'edges_only') { + $w = $im->getImageWidth(); + $h = $im->getImageHeight(); + $seeds = array(); + for ($x = 0; $x < $w; $x += 15) { + $seeds[] = array($x, 0); + $seeds[] = array($x, $h - 1); + } + for ($y = 0; $y < $h; $y += 15) { + $seeds[] = array(0, $y); + $seeds[] = array($w - 1, $y); + } + $seeds[] = array($w - 1, $h - 1); + + foreach ($seeds as $c) { + $borderPixel = $im->getImagePixelColor($c[0], $c[1]); + $colorArray = $borderPixel->getColor(); + if ($colorArray['r'] <= ($darkness + 40) && $colorArray['g'] <= ($darkness + 40) && $colorArray['b'] <= ($darkness + 40)) { + @$im->floodfillPaintImage($fillColor, $fuzz, $borderPixel, $c[0], $c[1], false); + } + } + } else { + $im->transparentPaintImage(new \ImagickPixel('rgb(0,0,0)'), 0.0, $fuzz, false); + $bg = new \Imagick(); + $bg->newImage($im->getImageWidth(), $im->getImageHeight(), $fillColor); + $bg->compositeImage($im, \Imagick::COMPOSITE_OVER, 0, 0); + $im = $bg; + } + + $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + if ($ext === 'png') { + $im->setImageFormat('png'); + } else { + $im->setImageFormat('jpeg'); + $im->setImageCompression(\Imagick::COMPRESSION_JPEG); + $im->setImageCompressionQuality(100); + $im->setOption('jpeg:sampling-factor', '4:4:4'); + } + $im->writeImage($filePath); + $im->clear(); + $im->destroy(); + return true; + } catch (\Exception $e) { + return false; + } + } + + public function scan_black_bg($data = array()) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $limit = isset($data['module_img_opti_bb_limit']) ? (int)$data['module_img_opti_bb_limit'] : 100; + if ($limit <= 0) $limit = 100; + + $darkness = isset($data['module_img_opti_bb_darkness']) ? (int)$data['module_img_opti_bb_darkness'] : 30; + $threshold = isset($data['module_img_opti_bb_threshold']) ? (int)$data['module_img_opti_bb_threshold'] : 60; + $scan_fake = isset($data['module_img_opti_bb_scan_fake']) ? (int)$data['module_img_opti_bb_scan_fake'] : 1; + $scan_black = isset($data['module_img_opti_bb_scan_black']) ? (int)$data['module_img_opti_bb_scan_black'] : 1; + + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)); + $this->load->model('tool/image'); + + foreach ($iterator as $file) { + if ($file->isFile()) { + $realPath = realpath($file->getPathname()); + if ($realPath && strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) === 0) { + $ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION)); + if (!in_array($ext, array('jpg', 'jpeg'))) continue; + + $relPath = $this->getRelativeImagePath($realPath); + if (preg_match('/(wp-content|elementor|thumbs|templates?)/i', $relPath)) continue; + + $is_fake = false; + $is_black = false; + + if ($scan_fake && in_array($ext, array('jpg', 'jpeg'))) { + $header = @file_get_contents($realPath, false, null, 0, 8); + if ($header === "\x89PNG\r\n\x1a\n") { + $is_fake = true; + } + } + + if ($scan_black && !$is_fake) { + $img = @imagecreatefromstring(@file_get_contents($realPath)); + if ($img) { + $w = imagesx($img); + $h = imagesy($img); + if ($w >= 20 && $h >= 20) { + $corners = array( + array(0, 0), array($w - 1, 0), + array(0, $h - 1), array($w - 1, $h - 1) + ); + $black_corners = 0; + foreach ($corners as $c) { + $rgb = imagecolorat($img, $c[0], $c[1]); + $alpha = ($rgb & 0x7F000000) >> 24; + $r = ($rgb >> 16) & 0xFF; + $g = ($rgb >> 8) & 0xFF; + $b = $rgb & 0xFF; + if ($alpha <= 10 && $r <= ($darkness + 15) && $g <= ($darkness + 15) && $b <= ($darkness + 15)) { + $black_corners++; + } + } + + if ($black_corners >= 3) { + $sample_points = array(); + for ($i = 0; $i < 20; $i++) { + $sample_points[] = array((int)($i * ($w - 1) / 19), 0); + $sample_points[] = array((int)($i * ($w - 1) / 19), $h - 1); + $sample_points[] = array(0, (int)($i * ($h - 1) / 19)); + $sample_points[] = array($w - 1, (int)($i * ($h - 1) / 19)); + } + $black_count = 0; + foreach ($sample_points as $p) { + $rgb = imagecolorat($img, $p[0], $p[1]); + $alpha = ($rgb & 0x7F000000) >> 24; + $r = ($rgb >> 16) & 0xFF; + $g = ($rgb >> 8) & 0xFF; + $b = $rgb & 0xFF; + if ($alpha <= 10 && $r <= $darkness && $g <= $darkness && $b <= $darkness) { + $black_count++; + } + } + $pct = ($black_count / count($sample_points)) * 100; + if ($pct >= $threshold) { + $is_black = true; + } + } + } + imagedestroy($img); + } + } + + if ($is_fake || $is_black) { + $thumb = $this->model_tool_image->resize($relPath, 100, 100); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
', $entities) : '' . $this->language->get('text_no_relations') . ''; + + $issue_type = $is_fake ? 'fake_jpg' : 'black_bg'; + $issue_title = $is_fake ? $this->language->get('text_bb_fake_jpg') : $this->language->get('text_bb_black_bg'); + $before_desc = $is_fake ? '' . $this->language->get('text_bb_type_fake') . '' : '' . $this->language->get('text_bb_type_black') . ''; + + if ($is_fake) { + $after_desc = ' ' . $this->language->get('text_bb_fix_flatten_white') . ''; + $actions = array( + 'fix_flatten_white' => $this->language->get('text_bb_fix_flatten_white'), + 'fix_convert_png' => $this->language->get('text_bb_fix_convert_png'), + 'ignore' => $this->language->get('text_action_ignore') + ); + $default_action = 'fix_flatten_white'; + } else { + $after_desc = ' ' . $this->language->get('text_bb_fix_white') . ''; + $actions = array( + 'fix_white' => $this->language->get('text_bb_fix_white'), + 'fix_white_all' => $this->language->get('text_bb_fix_white_all'), + 'fix_white_smooth' => $this->language->get('text_bb_fix_white_smooth') + ); + if (extension_loaded('imagick')) { + $actions['fix_white_imagick'] = $this->language->get('text_bb_fix_white_imagick'); + $actions['fix_white_all_imagick'] = $this->language->get('text_bb_fix_white_all_imagick'); + $actions['fix_white_smooth_imagick'] = $this->language->get('text_bb_fix_white_smooth_imagick'); + } + $actions['ignore'] = $this->language->get('text_action_ignore'); + $default_action = extension_loaded('imagick') ? 'fix_white_smooth_imagick' : 'fix_white_smooth'; + } + + $rows[] = array( + 'id' => 'black_bg-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
' . $entities_html, + 'before' => $issue_title . '
' . $before_desc, + 'after' => $after_desc, + 'actions' => $actions, + 'selected_action' => $default_action, + 'meta' => array( + 'path' => $relPath, + 'type' => $issue_type + ) + ); + + if (count($rows) >= $limit) { + break; + } + } + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_black_bg_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, + 'backup_available' => true, + 'quarantine_available' => true, + 'description' => $this->language->get('text_description_black_bg') + ); + } + + public function fix_black_bg($data) { + $this->backupCatalogTables('black_bg'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + $darkness = isset($data['module_img_opti_bb_darkness']) ? (int)$data['module_img_opti_bb_darkness'] : 30; + + $cache_file = DIR_CACHE . 'img_opti_black_bg_grid.json'; + if (!is_file($cache_file)) { + return $this->formatFixResult(0, 0); + } + + $grid = json_decode(file_get_contents($cache_file), true); + if (!is_array($grid)) { + return $this->formatFixResult(0, 0); + } + + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $realImageDir = realpath(DIR_IMAGE); + $processed = 0; + $allowed_actions = array('fix_flatten_white', 'fix_convert_png', 'fix_white', 'fix_white_all', 'fix_white_smooth', 'fix_white_imagick', 'fix_white_all_imagick', 'fix_white_smooth_imagick'); + + foreach ($selected as $row_id => $act) { + if (!in_array($act, $allowed_actions) || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $relPath = (string)$row['meta']['path']; + $issueType = (string)$row['meta']['type']; + $cleanBadFile = str_replace(array('../', '..\\', "\0"), '', $relPath); + $fullPath = DIR_IMAGE . $cleanBadFile; + + if (!is_file($fullPath)) continue; + $realFullPath = realpath($fullPath); + if (!$realFullPath || strpos(str_replace('\\', '/', $realFullPath), str_replace('\\', '/', $realImageDir)) !== 0) continue; + + $imgData = @file_get_contents($fullPath); + if ($imgData === false) continue; + + $this->quarantineFile($relPath, 'black_bg'); + + if ($act === 'fix_flatten_white') { + $pngImg = @imagecreatefromstring($imgData); + if ($pngImg) { + $w = imagesx($pngImg); + $h = imagesy($pngImg); + $jpgCanvas = imagecreatetruecolor($w, $h); + imagealphablending($jpgCanvas, true); + $white = imagecolorallocate($jpgCanvas, 255, 255, 255); + imagefilledrectangle($jpgCanvas, 0, 0, $w - 1, $h - 1, $white); + imagealphablending($pngImg, true); + imagesavealpha($pngImg, true); + imagecopy($jpgCanvas, $pngImg, 0, 0, 0, 0, $w, $h); + $jpgCanvas = $this->replaceBlackBackgroundAdaptive($jpgCanvas, '#FFFFFF', $darkness, 'edges_only'); + @imagejpeg($jpgCanvas, $fullPath, 100); + imagedestroy($pngImg); + imagedestroy($jpgCanvas); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } + } elseif ($act === 'fix_convert_png') { + $pngImg = @imagecreatefromstring($imgData); + if ($pngImg) { + imagesavealpha($pngImg, true); + imagealphablending($pngImg, false); + $newRelPath = preg_replace('/\.(jpe?g)$/i', '.png', $relPath); + $newFullPath = DIR_IMAGE . str_replace(array('../', '..\\', "\0"), '', $newRelPath); + $newDir = dirname($newFullPath); + if (!is_dir($newDir)) { + @mkdir($newDir, 0755, true); + } + @imagepng($pngImg, $newFullPath, 9); + imagedestroy($pngImg); + if (is_file($fullPath)) { + @unlink($fullPath); + } + $this->updateImagePathInDb($relPath, $newRelPath); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($newRelPath, 100, 100); + $processed++; + } + } elseif ($act === 'fix_white') { + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, 'edges_only'); + $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION)); + if ($ext === 'png') { + @imagepng($img, $fullPath, 9); + } else { + @imagejpeg($img, $fullPath, 100); + } + imagedestroy($img); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } + } elseif ($act === 'fix_white_all') { + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, 'all'); + $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION)); + if ($ext === 'png') { + @imagepng($img, $fullPath, 9); + } else { + @imagejpeg($img, $fullPath, 100); + } + imagedestroy($img); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } + } elseif ($act === 'fix_white_smooth') { + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, 'all_smooth'); + $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION)); + if ($ext === 'png') { + @imagepng($img, $fullPath, 9); + } else { + @imagejpeg($img, $fullPath, 100); + } + imagedestroy($img); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } + } elseif (in_array($act, array('fix_white_imagick', 'fix_white_all_imagick', 'fix_white_smooth_imagick'))) { + $imagick_mode = 'all_smooth'; + if ($act === 'fix_white_imagick') { + $imagick_mode = 'edges_only'; + } elseif ($act === 'fix_white_all_imagick') { + $imagick_mode = 'all'; + } + if ($this->replaceBlackBackgroundImagick($fullPath, '#FFFFFF', $darkness, $imagick_mode)) { + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } else { + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, $imagick_mode); + @imagejpeg($img, $fullPath, 100); + imagedestroy($img); + $this->deleteImageCache($relPath); + $this->load->model('tool/image'); + @$this->model_tool_image->resize($cleanBadFile, 100, 100); + $processed++; + } + } + } + } + + return $this->formatFixResult($processed, count($selected)); + } + + public function preview_black_bg_row($data) { + $row_id = isset($data['row_id']) ? (string)$data['row_id'] : ''; + $act = isset($data['action_type']) ? (string)$data['action_type'] : 'fix_white'; + $darkness = isset($data['module_img_opti_bb_darkness']) ? (int)$data['module_img_opti_bb_darkness'] : 30; + + $cache_file = DIR_CACHE . 'img_opti_black_bg_grid.json'; + if (!is_file($cache_file)) { + return array('error' => 'Cache file not found'); + } + + $grid = json_decode(file_get_contents($cache_file), true); + if (!is_array($grid)) { + return array('error' => 'Invalid grid cache'); + } + + $targetRow = null; + foreach ($grid as $row) { + if ($row['id'] === $row_id) { + $targetRow = $row; + break; + } + } + + if (!$targetRow) { + return array('error' => 'Row not found'); + } + + $relPath = (string)$targetRow['meta']['path']; + $cleanBadFile = str_replace(array('../', '..\\', "\0"), '', $relPath); + $fullPath = DIR_IMAGE . $cleanBadFile; + + if (!is_file($fullPath)) { + return array('error' => 'Original image file not found on disk'); + } + + $imgData = @file_get_contents($fullPath); + if ($imgData === false) { + return array('error' => 'Failed to read image file'); + } + + $this->load->model('tool/image'); + $thumbBefore = $this->model_tool_image->resize($cleanBadFile, 350, 350); + + $cache_dir = DIR_IMAGE . 'cache'; + if (!is_dir($cache_dir)) { + @mkdir($cache_dir, 0755, true); + } + + $old_previews = glob($cache_dir . '/img_opti_preview_*.jpg'); + if ($old_previews) { + $now = time(); + foreach ($old_previews as $old_file) { + if (($now - filemtime($old_file)) > 300) { + @unlink($old_file); + } + } + } + + $tempFilename = 'img_opti_preview_' . md5($relPath . '_' . $act . '_' . microtime(true)) . '.jpg'; + $tempRelPath = 'cache/' . $tempFilename; + $tempFullPath = DIR_IMAGE . $tempRelPath; + + if ($act === 'ignore') { + $thumbAfter = $thumbBefore; + } elseif (in_array($act, array('fix_white_imagick', 'fix_white_all_imagick', 'fix_white_smooth_imagick'))) { + $imagick_mode = 'all_smooth'; + if ($act === 'fix_white_imagick') { + $imagick_mode = 'edges_only'; + } elseif ($act === 'fix_white_all_imagick') { + $imagick_mode = 'all'; + } + @copy($fullPath, $tempFullPath); + if (!$this->replaceBlackBackgroundImagick($tempFullPath, '#FFFFFF', $darkness, $imagick_mode)) { + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, $imagick_mode); + @imagejpeg($img, $tempFullPath, 100); + @imagedestroy($img); + } + } + $thumbAfter = $this->model_tool_image->resize($tempRelPath, 350, 350); + } else { + $gd_mode = 'edges_only'; + if ($act === 'fix_white_all') { + $gd_mode = 'all'; + } elseif ($act === 'fix_white_smooth') { + $gd_mode = 'all_smooth'; + } + $img = @imagecreatefromstring($imgData); + if ($img) { + $img = $this->replaceBlackBackgroundAdaptive($img, '#FFFFFF', $darkness, $gd_mode); + @imagejpeg($img, $tempFullPath, 100); + @imagedestroy($img); + } + $thumbAfter = $this->model_tool_image->resize($tempRelPath, 350, 350); + } + + return array( + 'success' => true, + 'title' => $relPath, + 'thumb_before' => $thumbBefore, + 'thumb_after' => $thumbAfter + ); + } }