rcsofttech/php-image-guard

SSIM-based image quality verification with auto-retry compression loop for PHP 8.4+

Maintainers

Package info

github.com/rcsofttech85/php-image-guard

pkg:composer/rcsofttech/php-image-guard

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 4

Open Issues: 0

v1.0.0 2026-03-31 06:38 UTC

This package is auto-updated.

Last update: 2026-04-01 09:41:27 UTC


README

PHP 8.4 License: MIT PHPStan: Max Codacy Badge Codacy Badge

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.