snipershady / picdiet
The lightweight image compressor for the web (JPG/WebP)
Requires
- php: >=8.3
- ext-gd: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94.2
- phpstan/phpstan: ^2.1.40
- phpunit/phpunit: *
- rector/rector: ^2.3.8
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Lightweight PHP library for compressing and converting images to WebP or JPEG format, with automatic resizing while preserving the aspect ratio.
Supports two backends: GD (built-in, zero extra dependencies) and Imagick (higher resampling quality, EXIF stripping, animated GIF support, AVIF/HEIC input).
Requirements
| Requirement | Version | Notes |
|---|---|---|
| PHP | >= 8.3 | |
| ext-gd | * | Required for the GD backend |
| ext-imagick | * | Optional — required only for the Imagick backend |
Installing PHP extensions on Debian / Ubuntu
Replace
php8.3with your actual PHP version. Check it withphp -v.
ext-gd
sudo apt update
sudo apt install php8.3-gd
sudo phpenmod gd
sudo systemctl restart php8.3-fpm # or apache2, nginx, etc.
Verify:
php -m | grep gd
ext-imagick
Imagick requires both the PHP extension and the ImageMagick system library.
sudo apt update sudo apt install imagemagick php8.3-imagick sudo phpenmod imagick sudo systemctl restart php8.3-fpm
Verify:
php -m | grep imagick
HEIC / HEIF support (optional): requires
libheifand a version of ImageMagick compiled with HEIC support.sudo apt install libheif-dev
Installation
composer require snipershady/picdiet
Quick start
Automatic backend selection (recommended)
createBest() picks Imagick if the extension is loaded, otherwise falls back to GD:
use PicDiet\Service\ImageCompressorFactory; $service = ImageCompressorFactory::createBest(); $response = $service->compress('/path/to/photo.jpg'); if ($response->success) { echo $response->path; // /path/to/photo_compressed.webp echo $response->originalSize; // bytes echo $response->compressedSize; // bytes } else { echo $response->error; }
Explicit backend selection
use PicDiet\Enum\CompressionStrategy; use PicDiet\Service\ImageCompressorFactory; // GD backend (always available if ext-gd is installed) $service = ImageCompressorFactory::factory(CompressionStrategy::GD); // Imagick backend (throws RuntimeException if ext-imagick is not loaded) $service = ImageCompressorFactory::factory(CompressionStrategy::IMAGICK);
Check backend availability at runtime
use PicDiet\Enum\CompressionStrategy; use PicDiet\Service\ImageCompressorFactory; if (ImageCompressorFactory::isAvailable(CompressionStrategy::IMAGICK)) { $service = ImageCompressorFactory::factory(CompressionStrategy::IMAGICK); } else { $service = ImageCompressorFactory::factory(CompressionStrategy::GD); }
API Reference
compress() — all parameters
$response = $service->compress( sourcePath: '/path/to/image.png', // required format: ImageFormatEnum::WEBP, // WEBP (default) or JPG maxWidth: 1920, // default 1920 px maxHeight: 1080, // default 1080 px quality: 85, // 0–100, default 85 outputDirectory: '/path/to/output/', // default: same directory as source );
| Parameter | Type | Default | Description |
|---|---|---|---|
$sourcePath |
string |
— | Absolute path to the source image |
$format |
ImageFormatEnum |
WEBP |
Output format: WEBP or JPG |
$maxWidth |
int |
1920 |
Max output width in pixels. The image is never upscaled |
$maxHeight |
int |
1080 |
Max output height in pixels. Aspect ratio is always preserved |
$quality |
int|null |
85 |
Compression quality 0–100 |
$outputDirectory |
string|null |
same as source | Directory where the compressed file is written. Must exist and be writable |
Output filename: the compressed file is saved with the suffix _compressed appended to the original filename and the extension replaced with the chosen format.
/images/photo.jpg → /images/photo_compressed.webp
/images/banner.png → /images/banner_compressed.jpg
CompressionResponse
All properties are readonly. The constructor is private: instances are only created via the named constructors CompressionResponse::success() and CompressionResponse::failure() inside the service layer.
| Property | Type | Description |
|---|---|---|
$success |
bool |
true if compression succeeded |
$path |
string|null |
Absolute path to the compressed file (null on failure) |
$error |
string|null |
Error message when $success is false, otherwise null |
$originalSize |
int |
Source file size in bytes (0 on early failure before the file is read) |
$compressedSize |
int |
Output file size in bytes (0 on failure) |
$format |
ImageFormatEnum|null |
Format used for the output. Guaranteed non-null on success. On failure, null when the error occurs before the image is loaded (e.g. file not found); otherwise carries the intended format |
$compressedFileName |
string|null |
File name of the compressed file (null on failure) |
$outputDirectory |
string|null |
Directory where the compressed file was saved (null on failure) |
CompressionStrategy
use PicDiet\Enum\CompressionStrategy; CompressionStrategy::GD // PHP GD extension CompressionStrategy::IMAGICK // PHP Imagick extension
ImageFormatEnum
use PicDiet\Enum\ImageFormatEnum; ImageFormatEnum::WEBP // value: 'webp' ImageFormatEnum::JPG // value: 'jpg'
GD vs Imagick: choosing the right backend
Both backends implement the same interface and produce equivalent results in the most common scenarios (JPEG/PNG/WebP in, WebP/JPEG out). The choice depends on your infrastructure constraints and the quality bar you need to meet.
When to choose GD
- Zero-friction setup.
ext-gdships with virtually every PHP distribution and most shared hosting plans. No system packages to install, no ImageMagick version to manage. - Minimal memory footprint. GD keeps everything in the PHP heap: predictable memory usage, easy to tune with
memory_limit. - Enough for everyday web images. Thumbnail generation, upload pipelines, and CMS image processing rarely require the extra quality headroom that Imagick provides. GD handles these workloads well.
- Shared or managed hosting. When you cannot install system packages (e.g. plain cPanel hosting), GD is the only viable option.
When to choose Imagick
- Higher resampling quality. Imagick uses the Lanczos filter by default, which produces noticeably sharper results than GD's bicubic when downsizing to thumbnails or aggressive resolutions (e.g. 1200 px → 120 px). The difference is visible on product images, portfolio photos, and print-ready assets.
- Automatic EXIF stripping.
stripImage()removes all metadata (GPS coordinates, camera model, author, etc.) in a single call. With GD, metadata is silently preserved unless you handle it yourself. This matters for privacy-sensitive applications (medical imaging, user photo uploads). - Animated GIFs. GD can only read and write the first frame, silently discarding the animation. Imagick preserves all frames. If your application accepts GIF uploads, Imagick is the correct choice.
- Wider input format support. Imagick can decode AVIF, HEIC/HEIF (iPhone default format), TIFF, BMP, and many others out of the box, provided the corresponding system libraries are present. GD is limited to JPEG, PNG, WebP, and GIF.
- Large image processing. ImageMagick delegates heavy work to a native process, avoiding PHP's memory ceiling. Processing a 50 MP RAW-quality TIFF with GD will exhaust
memory_limit; Imagick will not. - Professional / editorial workflows. Photo agencies, print pipelines, and e-commerce platforms with high visual standards typically require Lanczos-quality resampling and reliable EXIF removal. Imagick is the appropriate backend for these contexts.
Summary table
| GD | Imagick | |
|---|---|---|
| Availability | Built into most PHP packages | Requires imagemagick system package |
| Installation complexity | None | apt install imagemagick php-imagick |
| Resampling algorithm | Bicubic | Lanczos (sharper results) |
| EXIF / metadata stripping | No | Yes (automatic) |
| Memory model | PHP heap | Delegated to ImageMagick process |
| Animated GIF input | First frame only | Full animation preserved |
| AVIF / HEIC input | No | Yes (requires system libs) |
| TIFF, BMP input | No | Yes |
| Best suited for | Shared hosting, simple pipelines, cost-sensitive deployments | High-quality image processing, privacy-sensitive apps, wide format support |
Rule of thumb: start with
createBest(). It automatically uses Imagick when available and falls back to GD otherwise, so your code is portable and improves silently as infrastructure grows.
Supported input formats
| Format | GD | Imagick |
|---|---|---|
| JPEG | yes | yes |
| PNG (with transparency) | yes | yes |
| WebP | yes | yes |
| GIF (first frame only) | yes | yes |
| GIF (animated, all frames) | no | yes |
| AVIF | no | yes |
| HEIC / HEIF | no | yes (requires libheif) |
| TIFF | no | yes |
Usage examples
Convert to WebP and measure savings
use PicDiet\Service\ImageCompressorFactory; $service = ImageCompressorFactory::createBest(); $response = $service->compress('/var/www/uploads/photo.jpg'); if ($response->success) { $saved = $response->originalSize - $response->compressedSize; $ratio = (1 - $response->compressedSize / $response->originalSize) * 100; printf("Original: %d bytes\n", $response->originalSize); printf("Compressed: %d bytes\n", $response->compressedSize); printf("Saved: %d bytes (%.1f%% reduction)\n", $saved, $ratio); printf("Output: %s\n", $response->path); }
Convert PNG to WebP (preserving transparency)
GD and Imagick both handle PNG alpha channels correctly when converting to WebP, which natively supports transparency.
use PicDiet\Enum\ImageFormatEnum; use PicDiet\Service\ImageCompressorFactory; $service = ImageCompressorFactory::createBest(); $response = $service->compress('/var/www/uploads/logo.png', ImageFormatEnum::WEBP); if ($response->success) { // logo_compressed.webp — fully transparent pixels preserved echo $response->path; }
Convert to JPEG
use PicDiet\Enum\ImageFormatEnum; use PicDiet\Enum\CompressionStrategy; use PicDiet\Service\ImageCompressorFactory; $service = ImageCompressorFactory::factory(CompressionStrategy::GD); $response = $service->compress('/var/www/uploads/photo.png', ImageFormatEnum::JPG);
Resize to a maximum resolution
The image is scaled down proportionally so that neither dimension exceeds the given maximum. Images smaller than the maximum are never upscaled.
use PicDiet\Enum\ImageFormatEnum; use PicDiet\Service\ImageCompressorFactory; $service = ImageCompressorFactory::createBest(); // Resize to at most 1280×720, keeping aspect ratio $response = $service->compress( sourcePath: '/var/www/uploads/photo.jpg', format: ImageFormatEnum::WEBP, maxWidth: 1280, maxHeight: 720, ); // A 1920×1080 source becomes 1280×720 // A 1920×800 source becomes 1280×533 (ratio preserved) // A 640×480 source stays 640×480 (no upscaling)
Generate a thumbnail
use PicDiet\Enum\ImageFormatEnum; use PicDiet\Service\ImageCompressorFactory; $service = ImageCompressorFactory::createBest(); $response = $service->compress( sourcePath: '/var/www/uploads/photo.jpg', format: ImageFormatEnum::WEBP, maxWidth: 300, maxHeight: 300, quality: 70, );
Custom output directory
By default the compressed file is saved alongside the source. Pass outputDirectory to write it elsewhere. The directory must already exist.
use PicDiet\Enum\ImageFormatEnum; use PicDiet\Service\ImageCompressorFactory; $service = ImageCompressorFactory::createBest(); $response = $service->compress( sourcePath: '/var/www/uploads/photo.jpg', format: ImageFormatEnum::WEBP, outputDirectory: '/var/www/compressed/', ); if ($response->success) { echo $response->outputDirectory; // /var/www/compressed/ echo $response->path; // /var/www/compressed/photo_compressed.webp }
Custom quality
quality accepts values from 0 (maximum compression, lowest quality) to 100 (minimum compression, best quality). The default is 85, which is a good balance for most web use cases.
use PicDiet\Enum\ImageFormatEnum; use PicDiet\Service\ImageCompressorFactory; $service = ImageCompressorFactory::createBest(); // High quality — larger file, ideal for print or editorial $response = $service->compress( sourcePath: '/var/www/uploads/photo.jpg', format: ImageFormatEnum::WEBP, quality: 95, ); // Low quality — smallest file, acceptable for previews and thumbnails $response = $service->compress( sourcePath: '/var/www/uploads/photo.jpg', format: ImageFormatEnum::WEBP, quality: 40, );
Error handling
$response->success is always set. Check it before accessing output properties — they are null on failure.
use PicDiet\Service\ImageCompressorFactory; $service = ImageCompressorFactory::createBest(); $response = $service->compress('/var/www/uploads/photo.jpg'); if (!$response->success) { // Possible error messages: // 'Source file does not exist' — $format is null, $originalSize is 0 // 'Failed to read source file size' — $format is null, $originalSize is 0 // 'Invalid image file' — GD backend; $format and $originalSize are set // 'Invalid image file, Exception: ...' — Imagick backend; $format and $originalSize are set // 'Failed to create image resource' — GD only; unsupported format (e.g. BMP) // 'Failed to save compressed image' — write error; $format and $originalSize are set // 'Failed to save compressed image, Exception: ...' — Imagick backend write error // 'Failed to read compressed file size' — post-write stat error error_log('PicDiet compression failed: ' . $response->error); return; } // On success: $path, $compressedFileName, $outputDirectory and $format are guaranteed non-null echo $response->path; echo $response->compressedFileName; echo $response->outputDirectory; echo $response->format->value; // 'webp' or 'jpg'
Batch processing a directory
use PicDiet\Enum\ImageFormatEnum; use PicDiet\Service\ImageCompressorFactory; $service = ImageCompressorFactory::createBest(); $outputDir = '/var/www/compressed/'; foreach (glob('/var/www/uploads/*.{jpg,jpeg,png}', GLOB_BRACE) as $sourcePath) { $response = $service->compress( sourcePath: $sourcePath, format: ImageFormatEnum::WEBP, maxWidth: 1920, maxHeight: 1080, quality: 85, outputDirectory: $outputDir, ); if ($response->success) { printf("OK %s → %s (%d bytes saved)\n", basename($sourcePath), $response->compressedFileName, $response->originalSize - $response->compressedSize, ); } else { printf("ERR %s: %s\n", basename($sourcePath), $response->error); } }
Framework integration
Symfony
Register the interface in the container and bind it to the factory:
# config/services.yaml services: PicDiet\Service\ImageCompressorInterface: factory: ['PicDiet\Service\ImageCompressorFactory', 'createBest']
Use it in a controller or service:
use PicDiet\Enum\ImageFormatEnum; use PicDiet\Service\ImageCompressorInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\AsController; #[AsController] class ImageUploadController { public function __construct( private readonly ImageCompressorInterface $compressor, ) {} public function __invoke(Request $request): void { $file = $request->files->get('image'); $uploadPath = '/var/www/uploads/' . $file->getClientOriginalName(); $file->move('/var/www/uploads', $file->getClientOriginalName()); $response = $this->compressor->compress( sourcePath: $uploadPath, format: ImageFormatEnum::WEBP, maxWidth: 1920, maxHeight: 1080, outputDirectory: '/var/www/compressed/', ); if (!$response->success) { throw new \RuntimeException($response->error); } // $response->path → absolute path to the compressed file } }
Laravel
Bind the factory in a service provider, then inject it wherever needed:
// app/Providers/AppServiceProvider.php use PicDiet\Service\ImageCompressorFactory; use PicDiet\Service\ImageCompressorInterface; public function register(): void { $this->app->bind(ImageCompressorInterface::class, function () { return ImageCompressorFactory::createBest(); }); }
Use it in a controller:
use PicDiet\Enum\ImageFormatEnum; use PicDiet\Service\ImageCompressorInterface; use Illuminate\Http\Request; class ImageController extends Controller { public function __construct( private readonly ImageCompressorInterface $compressor, ) {} public function upload(Request $request): void { $path = $request->file('image')->store('uploads'); $fullPath = storage_path('app/' . $path); $response = $this->compressor->compress( sourcePath: $fullPath, format: ImageFormatEnum::WEBP, maxWidth: 1920, maxHeight: 1080, outputDirectory: storage_path('app/compressed'), ); if (!$response->success) { abort(500, $response->error); } // $response->path → absolute path to the compressed file } }
Custom implementation
Implement ImageCompressorInterface to plug in your own backend (e.g. a cloud API, libvips, or a mock for testing):
use PicDiet\Dto\CompressionResponse; use PicDiet\Enum\ImageFormatEnum; use PicDiet\Service\ImageCompressorInterface; class MyCloudCompressor implements ImageCompressorInterface { #[\Override] public function compress( string $sourcePath, ImageFormatEnum $format = ImageFormatEnum::WEBP, int $maxWidth = 1920, int $maxHeight = 1080, ?int $quality = null, ?string $outputDirectory = null, ): CompressionResponse { // Call your cloud API, write the output file, then return: return CompressionResponse::success( path: '/tmp/output.webp', originalSize: filesize($sourcePath), compressedSize: 1234, format: $format, compressedFileName: 'output_compressed.webp', outputDirectory: '/tmp', ); } }
Development
git clone https://github.com/snipershady/picdiet.git
cd picdiet
composer install
| Command | Description |
|---|---|
composer test |
Run the PHPUnit test suite |
composer phpstan |
Run PHPStan static analysis |
composer cs-fix |
Fix code style with PHP-CS-Fixer |
composer cs-check |
Check code style without applying changes |
composer rector |
Run Rector refactoring |
composer rector-dry |
Preview Rector changes without applying them |
composer quality |
Run all quality tools (Rector + PHP-CS-Fixer) |
composer quality-check |
Check all quality rules without applying changes |
License
Released under the GPL-2.0-only license.
Author
Stefano Perrini — spinfo.it