Optimización eficiente de JPEG en PHP con jpegoptim
Optimizar imágenes es fundamental para mejorar el rendimiento web. Las imágenes JPEG, en particular, suelen representar una parte importante del tamaño total de una página web. Reducir su tamaño de archivo sin sacrificar la calidad visual puede mejorar drásticamente los tiempos de carga y la experiencia de usuario.
Introducción a la optimización de JPEG y su importancia
La optimización de JPEG consiste en comprimir imágenes para reducir el tamaño de archivo manteniendo una calidad visual aceptable. Las imágenes más pequeñas se cargan más rápido, consumen menos ancho de banda y mejoran el rendimiento web general. En sitios web con muchas imágenes, la optimización puede reducir los tiempos de carga de la página en varios segundos, lo que afecta directamente a la interacción de los usuarios y a las tasas de conversión. Este proceso es una pieza clave del ajuste del rendimiento web.
Descripción general de jpegoptim y sus capacidades
jpegoptim es una potente utilidad de línea de comandos diseñada específicamente para optimizar imágenes JPEG. Admite:
- Optimización sin pérdida de las tablas de compresión JPEG
- Compresión de imágenes con pérdida y ajustes de calidad configurables
- Eliminación de metadatos (EXIF, perfiles ICC, comentarios)
- Conversión a JPEG progresivo
- Especificación de un tamaño de archivo objetivo
Estas capacidades hacen de jpegoptim una excelente opción para los desarrolladores web que buscan mejorar el rendimiento de su sitio mediante la optimización de imágenes en PHP.
Configurar jpegoptim en un entorno PHP
Primero, instala jpegoptim en tu servidor. En sistemas basados en Debian:
sudo apt-get update
sudo apt-get install jpegoptim
En macOS con Homebrew:
brew install jpegoptim
Verifica la instalación:
jpegoptim --version
Requisitos del sistema
Antes de continuar, asegúrate de que tu entorno cumple estos requisitos:
- Una versión de PHP 8 con soporte
- jpegoptim 1.5.0+
- libjpeg-turbo o libjpeg 8d+
- Espacio en disco suficiente para archivos temporales
- Permisos de archivo adecuados para que el usuario del servidor web pueda ejecutar
jpegoptimy escribir en los archivos de imagen.
Ejemplos prácticos de PHP para optimizar imágenes JPEG
La siguiente clase aplica de forma predeterminada la recompresión sin pérdida. Al pasar max optas por
una recodificación con pérdida. Las llamadas sin una ruta de salida independiente modifican el
archivo original, así que conserva copias de seguridad antes de ejecutar operaciones por lotes. La
eliminación de metadatos también borra información como los perfiles de color.
<?php
declare(strict_types=1);
class ImageOptimizer {
private string $binaryPath;
private array $options;
// Options are application-controlled configuration, never request data.
public function __construct(array $options = []) {
// Sensible defaults
$this->options = array_merge([
'all-progressive' => true // Lossless recompression; metadata is retained
], $options);
$this->binaryPath = $this->findBinary();
}
private function findBinary(): string {
// Check if the jpegoptim binary exists and is executable
exec('command -v jpegoptim', $output, $returnVar);
if ($returnVar !== 0 || empty($output[0])) {
throw new RuntimeException('jpegoptim binary not found or not executable in system PATH.');
}
// Return the full path found
return trim($output[0]);
}
public function optimize(string $inputPath, ?string $outputPath = null): bool {
if (!file_exists($inputPath) || !is_readable($inputPath)) {
throw new InvalidArgumentException('Input file does not exist or is not readable: ' . $inputPath);
}
// Validate file is actually a JPEG using MIME type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $inputPath);
finfo_close($finfo);
if ($mimeType !== 'image/jpeg') {
throw new InvalidArgumentException(
"Invalid file type: {$mimeType}. Only JPEG files are supported for file: " . $inputPath
);
}
$targetPath = $outputPath ?? $inputPath;
// If output path is specified, copy the file first
if ($outputPath) {
// Ensure output directory exists and is writable
$outputDir = dirname($outputPath);
if (!is_dir($outputDir) || !is_writable($outputDir)) {
throw new RuntimeException("Output directory is not writable or does not exist: {$outputDir}");
}
if (!copy($inputPath, $outputPath)) {
throw new RuntimeException("Failed to copy file from {$inputPath} to output path: {$outputPath}");
}
// Ensure the copied file has appropriate permissions if needed
// chmod($outputPath, 0644);
}
$command = $this->buildCommand($targetPath);
exec($command, $cmdOutput, $returnVar);
if ($returnVar !== 0) {
// Clean up copied file on failure if output path was specified
if ($outputPath && file_exists($outputPath) && $targetPath === $outputPath) {
unlink($outputPath);
}
throw new RuntimeException(
"jpegoptim optimization failed with code {$returnVar} for file {$targetPath}: " . implode("\n", $cmdOutput)
);
}
return true;
}
private function buildCommand(string $path): string {
$optionStrings = [];
foreach ($this->options as $key => $value) {
// Handle boolean flags (like --strip-all)
if (is_bool($value) && $value) {
$optionStrings[] = '--' . $key;
// Handle options with values (like --max=85)
} elseif (!is_bool($value)) {
// Ensure value is properly escaped for shell argument
$optionStrings[] = '--' . $key . '=' . escapeshellarg((string)$value);
}
}
// Ensure the binary path and file path are properly escaped
return sprintf(
'%s %s %s',
escapeshellarg($this->binaryPath), // Path to the jpegoptim binary
implode(' ', $optionStrings),
escapeshellarg($path) // Path to the image file
);
}
}
// Usage example
try {
// Optimize with custom quality setting (lower quality = smaller size)
$optimizer = new ImageOptimizer(['max' => 80]);
$input = '/path/to/your/image.jpg';
$output = '/path/to/your/optimized_image.jpg'; // Optional: specify output path
// Ensure the input file exists and output directory is writable before calling
if (!file_exists($input)) {
throw new InvalidArgumentException("Input file does not exist: {$input}");
}
$outputDir = dirname($output);
if (!is_dir($outputDir) || !is_writable($outputDir)) {
throw new RuntimeException("Output directory is not writable: {$outputDir}");
}
// Optimize to a new file
if ($optimizer->optimize($input, $output)) {
echo "Image optimized successfully to {$output}.";
} else {
echo "Image optimization may have been skipped (e.g., no savings).";
}
// Example: Optimize in place
// $optimizerInPlace = new ImageOptimizer(['max' => 85]);
// if ($optimizerInPlace->optimize($input)) {
// echo "Image optimized successfully (in place).";
// }
} catch (InvalidArgumentException $e) {
error_log("Input error: " . $e->getMessage());
echo "Error: Invalid input provided. Check logs.";
} catch (RuntimeException $e) {
error_log("Optimization error: " . $e->getMessage());
echo "Error: Optimization failed. Check logs.";
} catch (Exception $e) { // Catch any other unexpected errors
error_log("General error: " . $e->getMessage());
echo "An unexpected error occurred.";
}
Integración moderna con composer
Para un enfoque más fácil de mantener, sobre todo en proyectos grandes, considera usar paquetes
existentes a través de Composer. El paquete spatie/image-optimizer ofrece un envoltorio cómodo para jpegoptim
y otras herramientas.
Instálalo:
composer require spatie/image-optimizer
Luego úsalo en tu código PHP:
<?php
require 'vendor/autoload.php';
use Spatie\ImageOptimizer\OptimizerChain;
use Spatie\ImageOptimizer\Optimizers\Jpegoptim;
use Psr\Log\NullLogger; // Or use your preferred PSR-3 logger
// Install jpegoptim separately. Omit --max to retain lossless compression.
$optimizerChain = (new OptimizerChain())
->addOptimizer(new Jpegoptim([
'--all-progressive',
]))
->useLogger(new NullLogger()); // Replace with a real PSR-3 logger in production
$imagePath = '/path/to/image.jpg';
try {
if (!file_exists($imagePath)) {
throw new InvalidArgumentException("Image file does not exist: {$imagePath}");
}
$optimizerChain->optimize($imagePath); // Optimizes in place
// To optimize to a different path:
// $optimizerChain->optimize($imagePath, '/path/to/optimized_image.jpg');
echo "Optimizer chain completed. Inspect logs and output for optimization results.";
} catch (Exception $e) {
error_log("Error using spatie/image-optimizer for {$imagePath}: " . $e->getMessage());
echo "An unexpected error occurred during optimization.";
}
Automatizar flujos de trabajo de optimización de imágenes con scripts de PHP
La cadena registra de forma predeterminada los fallos del optimizador; un retorno correcto por sí solo no demuestra que un optimizador externo se haya ejecutado correctamente. Usa un registrador real e inspecciona el archivo resultante.
La automatización es clave para gestionar múltiples imágenes, como las subidas de los usuarios. Este es un ejemplo completo para procesar un directorio entero de forma recursiva:
<?php
// Assumes the ImageOptimizer class from the previous example is available
// require_once 'ImageOptimizer.php';
function optimizeDirectory(string $directory, ?int $quality = null, bool $recursive = false): array {
$stats = [
'processed' => 0,
'skipped' => 0,
'failed' => 0,
'totalSavedBytes' => 0
];
if (!is_dir($directory) || !is_readable($directory)) {
error_log("Directory not found or not readable: {$directory}");
return $stats; // Return empty stats if directory is invalid
}
try {
// Initialize optimizer once
$optimizer = new ImageOptimizer($quality === null ? [] : ['max' => $quality]);
} catch (RuntimeException $e) {
error_log("Failed to initialize ImageOptimizer: " . $e->getMessage() . " Cannot process directory {$directory}.");
return $stats; // Cannot proceed without optimizer
}
$iteratorFlags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS;
$iterator = $recursive
? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory, $iteratorFlags))
: new DirectoryIterator($directory); // DirectoryIterator might be less efficient for large dirs
$finfo = finfo_open(FILEINFO_MIME_TYPE);
foreach ($iterator as $file) {
// Skip directories explicitly if using DirectoryIterator or ensure it's a file
if ($file->isDir() || !$file->isFile() || !$file->isReadable()) {
continue;
}
$path = $file->getPathname();
// Use mime type check for reliability
$mimeType = finfo_file($finfo, $path);
// Only process JPG/JPEG files
if ($mimeType !== 'image/jpeg') {
$stats['skipped']++;
continue;
}
try {
$sizeBefore = $file->getSize();
// Optimize in place
$optimizer->optimize($path);
// Clear stat cache to get updated file size
clearstatcache(true, $path);
$sizeAfter = filesize($path); // Re-check size after optimization
if ($sizeAfter === false) {
throw new RuntimeException("Could not get file size after optimization for {$path}");
}
$saved = $sizeBefore - $sizeAfter;
if ($saved > 0) {
$stats['processed']++;
$stats['totalSavedBytes'] += $saved;
} else {
// Count as skipped if no size reduction occurred or size increased slightly
$stats['skipped']++;
}
} catch (InvalidArgumentException $e) {
// This might indicate a file became unreadable or is not a valid JPEG despite MIME type
$stats['failed']++;
error_log("Skipping invalid file {$path}: {$e->getMessage()}");
} catch (RuntimeException $e) {
// Catch errors from the optimize method (e.g., jpegoptim failure)
$stats['failed']++;
error_log("Failed to optimize {$path}: {$e->getMessage()}");
} catch (Exception $e) {
// Catch any other unexpected errors
$stats['failed']++;
error_log("Unexpected error optimizing {$path}: {$e->getMessage()}");
}
}
finfo_close($finfo);
return $stats;
}
// Usage example
// Ensure the target directory exists and has appropriate permissions
$targetDirectory = '/path/to/your/uploads';
if (is_dir($targetDirectory) && is_writable($targetDirectory)) {
$results = optimizeDirectory($targetDirectory, null, true); // Lossless, recursive
echo "--- Optimization Results for {$targetDirectory} ---\n";
echo "Processed: {$results['processed']} images\n";
echo "Skipped: {$results['skipped']} files (non-JPEG or no savings)\n";
echo "Failed: {$results['failed']} images\n";
echo "Total space saved: " . round($results['totalSavedBytes'] / 1024 / 1024, 2) . " MB\n";
} else {
echo "Error: Target directory {$targetDirectory} does not exist or is not writable.\n";
}
Integración con laravel
Si usas el framework Laravel, integrar la optimización de imágenes es sencillo con el paquete
spatie/laravel-image-optimizer, que se apoya en spatie/image-optimizer.
Instala el paquete:
composer require spatie/laravel-image-optimizer
php artisan vendor:publish --provider="Spatie\LaravelImageOptimizer\ImageOptimizerServiceProvider"
Configura los optimizadores en config/image-optimizer.php (publicar el archivo de configuración lo deja
disponible):
<?php
// config/image-optimizer.php
return [
/*
* When calling `optimize` the package will automatically determine which optimizers
* should run for the given image.
*/
'optimizers' => [
Spatie\ImageOptimizer\Optimizers\Jpegoptim::class => [
// '--max=85', // Opt into lossy recompression only when intended
'--strip-all', // Remove all metadata
'--all-progressive', // Convert to progressive JPEGs
// '--force' // Uncomment if you want to force optimization even if it increases file size
],
// Other optimizers like Pngquant, Optipng, Gifsicle can be configured here
// Spatie\ImageOptimizer\Optimizers\Pngquant::class => [ ... ],
// Spatie\ImageOptimizer\Optimizers\Optipng::class => [ ... ],
// Spatie\ImageOptimizer\Optimizers\Gifsicle::class => [ ... ],
],
/*
* The maximum time in seconds each optimizer is allowed to run.
*/
'timeout' => 60,
/*
* Whether to log optimizer activity.
*/
'log_optimizer_activity' => env('IMAGE_OPTIMIZER_LOG_ACTIVITY', false),
];
Luego úsalo en tus controladores, trabajos o escuchadores de eventos, normalmente después de almacenar una imagen subida:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log; // Use Laravel's Log facade
use Spatie\LaravelImageOptimizer\Facades\ImageOptimizer as Optimizer; // Use the Facade with an alias
class UploadController extends Controller
{
public function store(Request $request)
{
$request->validate([
'image' => 'required|image|mimes:jpeg,jpg|max:10240', // Example validation (10MB max)
]);
if ($request->hasFile('image') && $request->file('image')->isValid()) {
$image = $request->file('image');
// Store the image using Laravel's filesystem (e.g., to 'public/uploads')
// Using store() generates a unique name automatically
$path = $image->store('uploads', 'public'); // Returns 'uploads/generated_filename.jpg'
if (!$path) {
return back()->with('error', 'Could not store the uploaded image.');
}
// Get the full path to the stored image on the server's filesystem
$fullPath = Storage::disk('public')->path($path);
// Optimize the image
try {
Optimizer::optimize($fullPath);
Log::info("Successfully optimized image: {$fullPath}");
} catch (\Exception $e) {
// Log the error if optimization fails
Log::error("Failed to optimize image {$fullPath}: " . $e->getMessage());
// Decide how to handle the failure (e.g., proceed without optimization, return error)
// For critical optimization, you might want to delete the file and return an error
// Storage::disk('public')->delete($path);
// return back()->with('error', 'Image uploaded but optimization failed.');
}
// Continue with your logic, e.g., save the path to the database
$imageUrl = Storage::disk('public')->url($path);
// Example: ImageModel::create(['path' => $path, 'url' => $imageUrl]);
return back()->with('success', 'Image uploaded and optimized successfully! URL: ' . $imageUrl);
} elseif ($request->hasFile('image')) {
// Handle upload errors (e.g., file too large, invalid type before storing)
return back()->with('error', 'Image upload failed: ' . $request->file('image')->getErrorMessage());
}
return back()->with('error', 'No valid image file was uploaded.');
}
}
Opciones avanzadas de personalización de jpegoptim en PHP
jpegoptim ofrece varias opciones avanzadas que puedes aprovechar desde PHP para un control más preciso:
--strip-all: Elimina todos los metadatos (EXIF, IPTC, perfiles ICC, comentarios).--strip-com: Elimina solo los marcadores de comentarios.--strip-exif: Elimina solo los marcadores EXIF.--strip-iptc: Elimina solo los marcadores IPTC.--strip-icc: Elimina solo los marcadores de perfil ICC.--all-progressive: Convierte las imágenes en JPEG progresivos (a menudo con mejor carga percibida).--all-normal: Convierte las imágenes en JPEG baseline estándar.--size=<size>: Intenta alcanzar un tamaño objetivo en kilobytes (por ejemplo,100) o un porcentaje del tamaño original (50%). Esto activa el modo con pérdida y tiene prioridad sobre--max.--max=<quality>: Establece el factor de calidad máximo (0-100). Activa el modo con pérdida. Los valores más bajos implican más compresión pero menor calidad. Es menos relevante si--sizeestá definido.--threshold=<percentage>: Establece la ganancia porcentual mínima de optimización necesaria para conservar el archivo optimizado (1-100). Si la ganancia es menor, se conserva el archivo original.--preserve-perms: Conserva los permisos del archivo original.--force: Fuerza la optimización aunque el resultado sea mayor que el original (útil sobre todo con--size).
Ejemplo que usa opciones avanzadas directamente con exec:
<?php
function advancedOptimizeJPEG(string $filePath, ?int $targetSizeKB = null, int $maxQuality = 85, bool $progressive = true, bool $stripAll = true, ?int $threshold = null): bool {
// Basic validation
if (!file_exists($filePath) || !is_readable($filePath)) {
throw new InvalidArgumentException('File does not exist or is not readable: ' . $filePath);
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
if ($mimeType !== 'image/jpeg') {
throw new InvalidArgumentException('Invalid or non-JPEG file provided: ' . $filePath);
}
$options = [];
// Metadata stripping
if ($stripAll) {
$options[] = '--strip-all';
}
// Progressive conversion
if ($progressive) {
$options[] = '--all-progressive';
} else {
$options[] = '--all-normal'; // Explicitly set to baseline if not progressive
}
// Target size (enables lossy)
if ($targetSizeKB !== null && $targetSizeKB > 0) {
$options[] = "--size=" . escapeshellarg((string)$targetSizeKB);
// Add --force if you strictly need to hit the size, even if quality drops significantly
// $options[] = '--force';
} else {
// Max quality (enables lossy if not already enabled by --size)
$options[] = "--max=" . escapeshellarg((string)$maxQuality);
}
// Threshold
if ($threshold !== null && $threshold >= 0 && $threshold <= 100) {
$options[] = "--threshold=" . escapeshellarg((string)$threshold);
}
// Add other options as needed, e.g., --preserve-perms
// $options[] = '--preserve-perms';
// Find jpegoptim binary path securely
exec('command -v jpegoptim', $binaryOutput, $binaryReturnVar);
if ($binaryReturnVar !== 0 || empty($binaryOutput[0])) {
throw new RuntimeException('jpegoptim binary not found or not executable.');
}
$binaryPath = trim($binaryOutput[0]);
$command = sprintf(
'%s %s %s',
escapeshellarg($binaryPath),
implode(' ', $options),
escapeshellarg($filePath)
);
exec($command, $output, $returnVar);
if ($returnVar !== 0) {
throw new RuntimeException("jpegoptim failed with code {$returnVar} for {$filePath}: " . implode("\n", $output));
}
return true; // Indicates command executed without critical error
}
// Usage example
try {
$imagePath1 = '/path/to/image_q80.jpg';
$imagePath2 = '/path/to/image_100k.jpg';
// Ensure files exist and are writable before calling
if (file_exists($imagePath1) && is_writable($imagePath1)) {
// Optimize to max 80 quality, progressive, strip metadata, skip if less than 2% saved
advancedOptimizeJPEG($imagePath1, null, 80, true, true, 2);
echo "Image {$imagePath1} optimized with advanced options (quality 80, threshold 2%).\n";
}
if (file_exists($imagePath2) && is_writable($imagePath2)) {
// Optimize targeting 100KB size, progressive, strip metadata
advancedOptimizeJPEG($imagePath2, 100, 85, true, true); // maxQuality is less relevant when size is set
echo "Image {$imagePath2} optimized with advanced options (target 100KB).\n";
}
} catch (InvalidArgumentException $e) {
echo "Error: " . $e->getMessage() . "\n";
} catch (RuntimeException $e) {
echo "Error during optimization: " . $e->getMessage() . "\n";
}
Medir los resultados de compresión
Mide un conjunto representativo de tus propias imágenes y conserva los originales. Registra los
bytes de entrada y de salida, el tiempo de ejecución, la versión de la herramienta y las opciones
exactas. Compara la recompresión sin pérdida por separado de las ejecuciones que usan --max o --size,
ya que esas opciones pueden reducir la calidad visual. Revisa el detalle fino, los degradados, el
texto, la orientación y el color antes de elegir la configuración para producción.
Consideraciones de seguridad
Cuando procesas archivos subidos por los usuarios o ejecutas binarios externos como jpegoptim desde PHP,
la seguridad es primordial:
- Valida a fondo los archivos de entrada:
- Nunca confíes en la entrada del usuario: comprueba las extensiones de archivo, pero
apóyate en
mime_content_type()ofinfo_file()para verificar que el archivo sea realmente un JPEG. Usais_uploaded_file()ymove_uploaded_file()para gestionar las subidas. - Rechaza los tipos inesperados: procesa solo los archivos identificados como
image/jpeg.
- Nunca confíes en la entrada del usuario: comprueba las extensiones de archivo, pero
apóyate en
- Sanea las rutas de archivo:
- Usa funciones como
basename()al construir rutas a partir de la entrada del usuario para evitar el salto de directorios (../../). Mejor aún, genera nombres de archivo únicos y seguros (por ejemplo, conuniqid()o bytes aleatorios) en lugar de usar los nombres proporcionados por el usuario. - Almacena las subidas fuera de la raíz web o en directorios no ejecutables, si es posible. Usa
realpath()para resolver enlaces simbólicos y canonicalizar rutas antes de pasarlas aexeco a operaciones de archivo, pero ten en cuenta sus limitaciones con rutas inexistentes.
- Usa funciones como
- Limita los recursos:
- Límites de tamaño de archivo: aplica tamaños máximos de subida en tu configuración de PHP
(
upload_max_filesize,post_max_size) y vuelve a validarlos en tu script antes de mover el archivo y antes de procesarlo. Rechaza los archivos excesivamente grandes. - Tiempos de espera de ejecución: usa
set_time_limit()en los scripts de PHP que procesan imágenes, sobre todo en bucles, para evitar que se ejecuten indefinidamente. Configura adecuadamente los tiempos de espera del servidor web y de PHP-FPM. - Limitación de tasa: implementa limitación de tasa en los endpoints de subida para evitar abusos.
- Límites de tamaño de archivo: aplica tamaños máximos de subida en tu configuración de PHP
(
- Ejecución segura:
- Usa
escapeshellarg()yescapeshellcmd(): sanea siempre los argumentos y comandos que pasas aexec(),shell_exec(), etc., para evitar vulnerabilidades de inyección de comandos. El ejemplo de la claseImageOptimizerlo demuestra. - Ejecuta con el mínimo privilegio: asegúrate de que el proceso del servidor web (por
ejemplo,
www-data) tenga los permisos mínimos necesarios. Solo debería poder ejecutarjpegoptimy leer y escribir archivos en directorios designados y protegidos. Evita ejecutar el servidor web como root.
- Usa
- Gestión de errores: implementa una gestión de errores robusta (como se muestra en los ejemplos) para detectar problemas durante la subida y la optimización, pero evita exponer rutas del sistema o mensajes de error detallados directamente a los usuarios. Registra los errores de forma segura en el lado del servidor.
Este es un fragmento más completo que ilustra una gestión segura en el contexto de una subida:
<?php
// Example within an upload processing function/controller action
/**
* Securely handles an uploaded image file, validates it, moves it,
* and attempts optimization.
*
* @param array $uploadedFileInfo The entry from $_FILES for the uploaded image.
* @param string $destinationDir The absolute path to the directory to store the final image.
* @param int $maxSize Maximum allowed file size in bytes.
* @return string|false The final path of the optimized image on success, false on failure.
*/
function handleAndOptimizeUpload(array $uploadedFileInfo, string $destinationDir, int $maxSize = 10485760 /* 10MB */): string|false
{
// 1. Check for basic upload errors
if (!isset($uploadedFileInfo['error']) || is_array($uploadedFileInfo['error'])) {
error_log('Invalid parameters received for file upload.');
return false;
}
switch ($uploadedFileInfo['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
error_log('No file sent.'); return false;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
error_log('Exceeded filesize limit.'); return false;
default:
error_log('Unknown upload error.'); return false;
}
// 2. Check file size against our limit
if ($uploadedFileInfo['size'] > $maxSize) {
error_log('Exceeded filesize limit: ' . $uploadedFileInfo['size'] . ' bytes.');
return false;
}
// 3. Verify it's a valid upload and check MIME type *before* moving
$tempPath = $uploadedFileInfo['tmp_name'];
if (!is_uploaded_file($tempPath)) {
error_log('Invalid upload: File is not an uploaded file.');
return false;
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $tempPath);
finfo_close($finfo);
if ($mimeType !== 'image/jpeg') {
error_log("Invalid MIME type ({$mimeType}) for uploaded file: " . $uploadedFileInfo['name']);
// No need to unlink tempPath, PHP handles it
return false;
}
// 4. Generate a secure, unique destination path
// Ensure destination directory exists and is writable
if (!is_dir($destinationDir) || !is_writable($destinationDir)) {
error_log("Destination directory is not writable or does not exist: {$destinationDir}");
return false;
}
// Create a unique filename to avoid collisions and using user input
$safeFilename = bin2hex(random_bytes(16)) . '.jpg';
$destinationPath = rtrim($destinationDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $safeFilename;
// 5. Move the uploaded file securely
if (!move_uploaded_file($tempPath, $destinationPath)) {
error_log("Failed to move uploaded file '{$uploadedFileInfo['name']}' to {$destinationPath}");
return false;
}
// 6. Optimize the moved file (using the robust class)
try {
// Set appropriate permissions if needed (e.g., 0644)
chmod($destinationPath, 0644);
$optimizer = new ImageOptimizer(['max' => 80]); // Load options from config ideally
$optimizer->optimize($destinationPath); // Optimize in place
// Log success
error_log("File uploaded and optimized successfully to: {$destinationPath}");
return $destinationPath; // Return the final path
} catch (Exception $e) {
error_log("Optimization failed for {$destinationPath} (original upload: {$uploadedFileInfo['name']}): " . $e->getMessage());
// Decide whether to keep the unoptimized file or delete it
// For consistency, maybe delete it if optimization is required
// unlink($destinationPath);
// return false;
// Or, keep the unoptimized file and return its path, logging the optimization failure
error_log("Kept unoptimized file at {$destinationPath} due to optimization error.");
return $destinationPath; // Return path even if optimization failed
}
}
// Example usage within a request handler (simplified):
/*
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['user_image'])) {
$uploadDir = '/var/www/my_app/storage/uploads'; // Secure, non-web-accessible ideally
$finalPath = handleAndOptimizeUpload($_FILES['user_image'], $uploadDir);
if ($finalPath) {
echo "File processed successfully. Final path: " . htmlspecialchars($finalPath);
// Store $finalPath in DB, etc.
} else {
echo "File processing failed. Check server logs.";
// http_response_code(500); // Or appropriate error code
}
}
*/
?>
Al combinar la eficiencia de jpegoptim con prácticas seguras de PHP para gestionar subidas y
procesos externos, puedes mejorar significativamente la compresión de imágenes, el rendimiento web
y la experiencia de usuario de tu sitio web mediante la automatización.
Para flujos de trabajo de procesamiento de medios más complejos que la simple optimización de JPEG, considera explorar servicios en la nube como Transloadit, que ofrecen una amplia gama de capacidades de manipulación de archivos a través de API robustas.
