Efficient JPEG optimization in PHP with jpegoptim

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:
- Validate Input Files Thoroughly:
- Never trust user input: Check file extensions, but rely on
mime_content_type()
orfinfo_file()
to verify the file is actually a JPEG. Useis_uploaded_file()
andmove_uploaded_file()
for handling uploads. - Reject unexpected types: Only process files identified as
image/jpeg
.
- Never trust user input: Check file extensions, but rely on
- 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., usinguniqid()
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 toexec
or file operations, but be aware of its limitations on non-existent paths.
- Use functions like
- 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.
- File Size Limits: Enforce maximum upload sizes in your PHP configuration
(
- Secure Execution:
- Use
escapeshellarg()
andescapeshellcmd()
: Always sanitize arguments and commands passed toexec()
,shell_exec()
, etc., to prevent command injection vulnerabilities. TheImageOptimizer
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 executejpegoptim
and read/write files in designated, secured directories. Avoid running the web server as root.
- Use
- 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.