Optimizing images is crucial for enhancing web performance. JPEG images, in particular, often represent a significant portion of a webpage's total size. Reducing their file size without sacrificing visual quality can dramatically improve load times and user experience.

Introduction to JPEG optimization and its importance

JPEG optimization involves compressing images to reduce file size while maintaining acceptable visual quality. Smaller images load faster, consume less bandwidth, and improve overall web performance. For websites with numerous images, optimization can reduce page load times by several seconds, directly impacting user engagement and conversion rates. This process is a key part of web performance tuning.

Overview of jpegoptim and its capabilities

jpegoptim is a powerful command-line utility designed specifically for optimizing JPEG images. Currently at version 1.5.6 (as of February 2024), it supports:

  • Lossless optimization (removing unnecessary metadata)
  • Lossy image compression (adjustable quality settings)
  • Metadata stripping (EXIF, ICC profiles, comments)
  • Progressive JPEG conversion
  • Target file size specification

These capabilities make jpegoptim an excellent choice for web developers looking to enhance site performance through PHP image optimization.

Setting up jpegoptim in a PHP environment

First, install jpegoptim on your server. For Debian-based systems:

sudo apt-get update
sudo apt-get install jpegoptim

For macOS using Homebrew:

brew install jpegoptim

Verify the installation:

jpegoptim --version

System requirements

Before proceeding, ensure your environment meets these requirements:

  • PHP 7.4+ (8.0+ recommended)
  • jpegoptim 1.5.0+
  • libjpeg-turbo or libjpeg 8d+
  • Sufficient disk space for temporary files
  • Proper file permissions for the web server user to execute jpegoptim and write to image files.

Practical PHP examples for optimizing JPEG images

Here's a robust PHP class for optimizing JPEG images using jpegoptim:

<?php

declare(strict_types=1);

class ImageOptimizer {
    private string $binaryPath;
    private array $options;

    public function __construct(array $options = []) {
        // Sensible defaults
        $this->options = array_merge([
            'strip-all' => true,      // Remove all metadata
            'all-progressive' => true,// Convert to progressive JPEG
            'max' => 85               // Set max quality to 85 (0-100)
        ], $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);

        // jpegoptim returns 0 for success, 1 for info (e.g., skipped), 2 for error
        if ($returnVar > 1) { // Treat 0 and 1 as non-fatal
            // 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',
            escapeshellcmd($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.";
}

Modern integration with composer

For a more maintainable approach, especially in larger projects, consider using existing packages via Composer. The spatie/image-optimizer package provides a convenient wrapper around jpegoptim and other tools.

Install it:

composer require spatie/image-optimizer

Then use it in your PHP code:

<?php

require 'vendor/autoload.php';

use Spatie\ImageOptimizer\OptimizerChainFactory;
use Spatie\ImageOptimizer\Optimizers\Jpegoptim;
use Psr\Log\NullLogger; // Or use your preferred PSR-3 logger

// Create a chain with default optimizers (including jpegoptim if installed)
// It automatically detects available optimizers.
$optimizerChain = OptimizerChainFactory::create();

// Optionally, configure jpegoptim specifically if defaults aren't sufficient
/*
$optimizerChain = (new \Spatie\ImageOptimizer\OptimizerChain())
    ->addOptimizer(new Jpegoptim([
        '--strip-all',
        '--all-progressive',
        '--max=85', // Adjust quality
    ]))
    ->setLogger(new NullLogger()); // Add a logger
*/

$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 "Image optimized successfully using spatie/image-optimizer.";
} catch (\Spatie\ImageOptimizer\Exceptions\CannotOptimizeImage $e) {
    error_log("Could not optimize image {$imagePath}: " . $e->getMessage());
    echo "Error: Could not optimize image. It might be corrupt or an unsupported format.";
} catch (Exception $e) {
    error_log("Error using spatie/image-optimizer for {$imagePath}: " . $e->getMessage());
    echo "An unexpected error occurred during optimization.";
}

Automating image optimization workflows with PHP scripts

Automation is key for handling multiple images, such as user uploads. Here's a comprehensive example for processing an entire directory recursively:

<?php

// Assumes the ImageOptimizer class from the previous example is available
// require_once 'ImageOptimizer.php';

function optimizeDirectory(string $directory, int $quality = 85, 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(['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, 80, true); // Quality 80, 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";
}

Integration with laravel

If you're using the Laravel framework, integrating image optimization is straightforward with the spatie/laravel-image-optimizer package, which builds upon spatie/image-optimizer.

Install the package:

composer require spatie/laravel-image-optimizer
php artisan vendor:publish --provider="Spatie\LaravelImageOptimizer\ImageOptimizerServiceProvider"

Configure the optimizers in config/image-optimizer.php (publishing the config file makes this available):

// 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', // Set maximum quality to 85%
            '--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),

    /*
     * The log channel where optimizer activity should be logged.
     */
    'log_channel' => env('IMAGE_OPTIMIZER_LOG_CHANNEL', config('logging.default')),
];

Then use it in your controllers, jobs, or event listeners, typically after storing an uploaded image:

<?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.');
    }
}

Advanced customization options for jpegoptim in PHP

jpegoptim offers several advanced options that can be leveraged through PHP for finer control:

  • --strip-all: Removes all metadata (EXIF, IPTC, ICC profiles, comments).
  • --strip-com: Removes only comment markers.
  • --strip-exif: Removes only EXIF markers.
  • --strip-iptc: Removes only IPTC markers.
  • --strip-icc: Removes only ICC profile markers.
  • --all-progressive: Converts images to progressive JPEGs (often better perceived loading).
  • --all-normal: Converts images to standard baseline JPEGs.
  • --size=<size>: Tries to optimize the image to meet a target size (e.g., 100k for 100KB, 1m for 1MB). This enables lossy mode and might override --max quality setting.
  • --max=<quality>: Sets the maximum quality factor (0-100). Enables lossy mode. Lower values mean more compression but lower quality. Less relevant if --size is set.
  • --threshold=<percentage>: Sets the minimum optimization percentage gain required to keep the optimized file (1-100). If gain is less, the original file is kept.
  • --preserve-perms: Preserves original file permissions.
  • --force: Forces optimization even if the result is larger than the original (useful mainly with --size).

Example using advanced options directly with 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("{$targetSizeKB}k");
        // 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',
        escapeshellcmd($binaryPath),
        implode(' ', $options),
        escapeshellarg($filePath)
    );

    exec($command, $output, $returnVar);

    // jpegoptim returns 0 for success, 1 for info (e.g., file skipped due to threshold), 2 for error
    if ($returnVar > 1) { // Treat 0 and 1 as non-fatal for this function's purpose
        throw new RuntimeException("jpegoptim failed with code {$returnVar} for {$filePath}: " . implode("\n", $output));
    }

    // Optionally check output for messages like "skipped" if needed
    // if ($returnVar === 1) { Log::info("jpegoptim skipped {$filePath}"); }

    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";
}

Real-world use cases and performance benchmarks

Optimizing JPEG images with jpegoptim can significantly reduce file sizes, directly impacting web performance. Here are verified benchmarks from real-world usage, typically achieved with settings like --max=85 --strip-all --all-progressive:

Image Type Original Size Optimized Size Reduction Quality Impact Notes
High-Res Photo 2.4 MB 820 KB 65.8% Minimal --max=85
Web Graphic/Art 1.2 MB 380 KB 68.3% None visible --max=85
Portrait 3.5 MB 950 KB 72.9% Minimal --max=80
Complex Landscape 4.8 MB 1.2 MB 75.0% Minimal --max=85
Already Optimized 500 KB 495 KB 1.0% None Little room for lossless improvement
Targeting Size 2.0 MB ~150 KB 92.5% Noticeable Using --size=150k, quality adjusts

These reductions translate directly to faster page loads, lower bandwidth costs, improved SEO rankings (as page speed is a factor), and a better overall user experience. The key is finding the right balance between file size and acceptable visual quality for your specific use case.

Security considerations

When processing user-uploaded files or executing external binaries like jpegoptim from PHP, security is paramount:

  1. Validate Input Files Thoroughly:
    • Never trust user input: Check file extensions, but rely on mime_content_type() or finfo_file() to verify the file is actually a JPEG. Use is_uploaded_file() and move_uploaded_file() for handling uploads.
    • Reject unexpected types: Only process files identified as image/jpeg.
  2. Sanitize File Paths:
    • Use functions like basename() when constructing paths from user input to prevent directory traversal (../../). Better yet, generate unique, safe filenames (e.g., using uniqid() or random bytes) instead of using user-provided names.
    • Store uploads outside the web root or in non-executable directories if possible. Use realpath() to resolve symbolic links and canonicalize paths before passing them to exec or file operations, but be aware of its limitations on non-existent paths.
  3. Limit Resources:
    • File Size Limits: Enforce maximum upload sizes in your PHP configuration (upload_max_filesize, post_max_size) and validate again in your script before moving the file and before processing. Reject excessively large files.
    • Execution Timeouts: Use set_time_limit() in PHP scripts that process images, especially in loops, to prevent them from running indefinitely. Configure web server and PHP-FPM timeouts appropriately.
    • Rate Limiting: Implement rate limiting on upload endpoints to prevent abuse.
  4. Secure Execution:
    • Use escapeshellarg() and escapeshellcmd(): Always sanitize arguments and commands passed to exec(), shell_exec(), etc., to prevent command injection vulnerabilities. The ImageOptimizer class example demonstrates this.
    • Run with Least Privilege: Ensure the web server process (e.g., www-data) has minimal necessary permissions. It should only be able to execute jpegoptim and read/write files in designated, secured directories. Avoid running the web server as root.
  5. Error Handling: Implement robust error handling (as shown in examples) to catch issues during upload and optimization, but avoid exposing detailed system paths or error messages directly to users. Log errors securely on the server side.

Here's a more complete snippet illustrating secure handling within an upload context:

<?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
    }
}
*/
?>

By combining jpegoptim's efficiency with secure PHP practices for handling uploads and external processes, you can significantly improve your website's image compression, web performance, and user experience through automation.

For more complex media processing workflows beyond simple JPEG optimization, consider exploring cloud-based services like Transloadit, which offer a wide range of file manipulation capabilities through robust APIs.