rcsofttech / php-image-guard
SSIM-based image quality verification with auto-retry compression loop for PHP 8.4+
Requires
- php: ^8.4
- ext-gd: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^11.0
Suggests
- ext-imagick: For higher-accuracy SSIM calculations via Imagick pixel export
README
SSIM-based image quality verification with auto-retry compression loop for PHP 8.4+
This package does ONE job: guard image quality. It does not compress, resize, or upload images. It verifies that a compression result meets your quality threshold — and retries if not.
Requirements
- PHP 8.4+
ext-gd(required)ext-imagick(optional — enables higher-precision SSIM via float pixel data)
Installation
composer require rcsofttech/php-image-guard
Quick Start
Minimal — check one image
use RcSoftTech\ImageGuard\ImageGuard; $gdImage = imagecreatefromjpeg('original.jpg'); $result = ImageGuard::check( 'original.jpg', function (int $quality, string $originalPath) use ($gdImage): string { $out = "/tmp/compressed_q{$quality}.jpg"; imagejpeg($gdImage, $out, $quality); return $out; } ); echo $result->summary(); // "Passed (SSIM: 0.961, Quality: 75, Saved: 68%, 143ms)"
Standalone SSIM score (no retry)
$score = ImageGuard::compare('original.jpg', 'compressed.jpg'); // float e.g. 0.961423
Full Fluent API
use RcSoftTech\ImageGuard\Enums\OnFailBehavior; use RcSoftTech\ImageGuard\ImageGuard; $result = ImageGuard::original('original.jpg') ->compressWith($compressor) ->threshold(0.92) // or: 'strict' | 'balanced' | 'loose' ->startAt(75) // initial quality integer ->maxQuality(95) // ceiling for retry loop ->step(5) // quality bump per retry ->onFail(OnFailBehavior::RECOMPRESS) ->run(ImageGuard::resolveCalculator()); if ($result->failed()) { logger()->warning('Image below threshold', $result->toArray()); }
Quality Presets
| String | SSIM Threshold |
|---|---|
'strict' |
0.97 |
'balanced' |
0.92 |
'loose' |
0.85 |
Custom numeric thresholds are typically used in the 0.0 to 1.0 range.
Failure Behaviors
OnFailBehavior |
Action |
|---|---|
RECOMPRESS |
Retry up to maxQuality, then accept best result |
WARN |
Return passed=false + add $result->warnings |
ABORT |
Throw exception holding the full PipelineResult |
Batch API
$report = ImageGuard::batch(glob('uploads/*.jpg')) ->compressWith($compressor) ->threshold('balanced') ->onFail(OnFailBehavior::WARN) ->run(ImageGuard::resolveCalculator()); echo $report->totalSaved(); // "38.1 MB saved across 147 images" echo $report->failRate(); // "3 of 147 failed (2.0%)" echo $report->averageSsim(); // 0.941200 file_put_contents('audit.json', $report->toJson()); file_put_contents('audit.csv', $report->toCsv());
Compressor Examples
The compressor callable receives a quality integer and the originalPath
string, and must return the path to the compressed file. This decouples
php-image-guard from any specific tool.
Plain GD
$compressor = function (int $quality, string $originalPath) use ($tmpDir): string { $gdImage = imagecreatefromjpeg($originalPath); $output = "{$tmpDir}/compressed_q{$quality}.jpg"; imagejpeg($gdImage, $output, $quality); imagedestroy($gdImage); return $output; };
With spatie/image-optimizer
use Spatie\ImageOptimizer\OptimizerChain; use Spatie\ImageOptimizer\Optimizers\Jpegoptim; $compressor = function (int $quality, string $originalPath) use ($tmpDir): string { $output = "{$tmpDir}/compressed_q{$quality}.jpg"; copy($originalPath, $output); (new OptimizerChain)->addOptimizer( new Jpegoptim(['--max=' . $quality]) )->optimize($output); return $output; };
With intervention/image
$compressor = function ( int $quality, string $originalPath, ) use ($manager, $tmpDir): string { $output = "{$tmpDir}/compressed_q{$quality}.webp"; $manager->read($originalPath)->toWebp($quality)->save($output); return $output; };
PipelineResult Shape
$result->passed // bool $result->ssimScore // float e.g. 0.961423 $result->threshold // float e.g. 0.92 $result->qualityUsed // int — final quality used $result->attempts // int — number of retry attempts $result->originalSize // int — bytes $result->compressedSize // int — bytes $result->savingsPercent // float e.g. 68.4 $result->formatInput // string e.g. 'jpeg' $result->formatOutput // string e.g. 'webp' $result->durationMs // int — total pipeline time $result->outputPath // string — path to accepted output $result->warnings // string[] — any warnings $result->summary() // "Passed (SSIM: 0.961, Quality: 75, Saved: 68%, 143ms)" $result->savings() // "68%" $result->passed() // bool $result->failed() // bool $result->toArray() // array<string, mixed> $result->toJson() // JSON string
SSIM Accuracy
These are practical expectations aligned with the current test suite and generated fixtures (not universal guarantees for every image set).
| Scenario | Expected |
|---|---|
| Identical images | SSIM = 1.000000 |
| White vs black | SSIM < 0.05 |
| JPEG quality 95 | SSIM > 0.98 |
| JPEG quality 10 | SSIM < 0.92 |
| PNG vs 50% WebP | 0.85–0.995 |
License
MIT — see LICENSE.