['maxWidth' => 1600, 'quality' => 82], 'noticias' => ['maxWidth' => 1200, 'quality' => 82], 'promos' => ['maxWidth' => 1200, 'quality' => 82], 'clubes' => ['maxWidth' => 512, 'quality' => 85], 'sponsors' => ['maxWidth' => 600, 'quality' => 85], 'qr' => ['maxWidth' => 800, 'quality' => 85], ]; public function handle(): int { $apply = (bool) $this->option('apply'); $only = $this->option('folder'); $noBackup = (bool) $this->option('no-backup'); if (!extension_loaded('gd')) { $this->error('La extension GD de PHP no esta instalada.'); return self::FAILURE; } // Usa la config real del disk 'public' (respeta override de Hostinger) $base = realpath(config('filesystems.disks.public.root')) ?: config('filesystems.disks.public.root'); if (!is_dir($base)) { $this->error("No existe: $base"); return self::FAILURE; } $this->line("Base: $base"); $backupBase = $base . '/_backup_optimize'; if ($apply && !$noBackup && !is_dir($backupBase)) { mkdir($backupBase, 0755, true); } $this->line(''); $this->line($apply ? 'MODO APLICACION — los archivos seran reemplazados.' : 'MODO DRY-RUN — no se modifica nada. Pasa --apply para aplicar.'); $this->line(''); $totalOrig = 0; $totalNew = 0; $totalProcessed = 0; $totalSkipped = 0; foreach ($this->config as $folder => $cfg) { if ($only && $only !== $folder) continue; $path = $base . '/' . $folder; if (!is_dir($path)) { $this->warn(" - $folder/ (no existe, skip)"); continue; } $this->info("Carpeta: $folder/ (max {$cfg['maxWidth']}px, q={$cfg['quality']})"); $files = glob($path . '/*.{jpg,jpeg,png,webp,JPG,JPEG,PNG,WEBP}', GLOB_BRACE); $folderOrig = 0; $folderNew = 0; $folderProcessed = 0; $folderSkipped = 0; foreach ($files as $file) { $origSize = filesize($file); $totalOrig += $origSize; $folderOrig += $origSize; $info = @getimagesize($file); if (!$info) { $folderSkipped++; $totalSkipped++; continue; } [$w, $h] = $info; $needsResize = $w > $cfg['maxWidth']; $needsRecomp = $origSize > 100 * 1024; // skip si ya pesa <100KB if (!$needsResize && !$needsRecomp) { $folderSkipped++; $totalSkipped++; $totalNew += $origSize; $folderNew += $origSize; continue; } $result = $this->processImage($file, $cfg['maxWidth'], $cfg['quality']); if ($result === null) { $folderSkipped++; $totalSkipped++; $totalNew += $origSize; $folderNew += $origSize; continue; } [$newBytes, $newW, $newH] = $result; if ($newBytes >= $origSize) { $folderSkipped++; $totalSkipped++; $totalNew += $origSize; $folderNew += $origSize; $this->line(sprintf(" - %-40s %dKB no mejora, skip", basename($file), $origSize / 1024)); continue; } $folderProcessed++; $totalProcessed++; $totalNew += $newBytes; $folderNew += $newBytes; $reduction = round((1 - $newBytes / $origSize) * 100, 1); $this->line(sprintf( " %s %-40s %dKB -> %dKB (-%s%%) %dx%d -> %dx%d", $apply ? 'OK' : '--', basename($file), $origSize / 1024, $newBytes / 1024, $reduction, $w, $h, $newW, $newH )); if ($apply) { if (!$noBackup) { $backupDir = $backupBase . '/' . $folder; if (!is_dir($backupDir)) mkdir($backupDir, 0755, true); copy($file, $backupDir . '/' . basename($file)); } file_put_contents($file, $result[3]); } } $this->line(sprintf( " -> %s files procesados, %s skip, %s -> %s (-%s%%)", $folderProcessed, $folderSkipped, $this->fmt($folderOrig), $this->fmt($folderNew), $folderOrig > 0 ? round((1 - $folderNew / max($folderOrig, 1)) * 100, 1) : 0 )); $this->line(''); } $this->line(str_repeat('=', 60)); $this->info(sprintf( 'TOTAL: %d procesados, %d skip. %s -> %s (ahorro: %s, -%s%%)', $totalProcessed, $totalSkipped, $this->fmt($totalOrig), $this->fmt($totalNew), $this->fmt($totalOrig - $totalNew), $totalOrig > 0 ? round((1 - $totalNew / max($totalOrig, 1)) * 100, 1) : 0 )); if (!$apply && $totalProcessed > 0) { $this->line(''); $this->warn('Para aplicar realmente: php artisan optimize:images --apply'); } return self::SUCCESS; } private function processImage(string $file, int $maxWidth, int $quality): ?array { $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); $img = match ($ext) { 'jpg', 'jpeg' => @imagecreatefromjpeg($file), 'png' => @imagecreatefrompng($file), 'webp' => @imagecreatefromwebp($file), default => null, }; if (!$img) return null; $w = imagesx($img); $h = imagesy($img); $newW = $w; $newH = $h; if ($w > $maxWidth) { $newW = $maxWidth; $newH = (int) round($h * ($maxWidth / $w)); $resized = imagecreatetruecolor($newW, $newH); if (in_array($ext, ['png', 'webp'])) { imagealphablending($resized, false); imagesavealpha($resized, true); $transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127); imagefilledrectangle($resized, 0, 0, $newW, $newH, $transparent); } imagecopyresampled($resized, $img, 0, 0, 0, 0, $newW, $newH, $w, $h); $img = $resized; } ob_start(); match ($ext) { 'jpg', 'jpeg' => imagejpeg($img, null, $quality), 'png' => imagepng($img, null, 9), 'webp' => imagewebp($img, null, $quality), }; $bytes = ob_get_clean(); return [strlen($bytes), $newW, $newH, $bytes]; } private function fmt(int $bytes): string { if ($bytes < 1024) return $bytes . 'B'; if ($bytes < 1024 * 1024) return round($bytes / 1024, 1) . 'KB'; return round($bytes / 1024 / 1024, 2) . 'MB'; } }