From 23c490d0bd15ea1af849e188c25d99fad21bbef3 Mon Sep 17 00:00:00 2001 From: Mathias Grimm Date: Wed, 22 Jul 2026 09:59:10 -0300 Subject: [PATCH 1/2] Surface the psnr field on ImageResult The API's optimize and convert responses now carry psnr, the peak signal-to-noise ratio in decibels between the input and the output. The property defaults to null and sits last in the constructor, so existing positional and named-argument construction keeps working. Responses without the field (resize, thumbnail, older API versions) map to null. Co-Authored-By: Claude Fable 5 --- src/ImageResult.php | 11 +++++++++++ tests/ClientTest.php | 24 ++++++++++++++++++++++-- tests/Pest.php | 5 +++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/ImageResult.php b/src/ImageResult.php index ecb9a4c..7141572 100644 --- a/src/ImageResult.php +++ b/src/ImageResult.php @@ -4,6 +4,13 @@ final readonly class ImageResult { + /** + * @param float|null $psnr Peak signal-to-noise ratio in decibels between the + * input and the output, returned by optimize and convert. + * Higher means closer to the input. Null when the API + * does not send the field (resize, thumbnail) or when + * the loss cannot be measured. + */ public function __construct( public string $bytes, public string $format, @@ -11,6 +18,7 @@ public function __construct( public int $size, public int $width, public int $height, + public ?float $psnr = null, ) {} /** @@ -18,6 +26,8 @@ public function __construct( */ public static function fromResponse(array $data): self { + $psnr = data_get($data, 'psnr'); + return new self( bytes: (string) base64_decode((string) data_get($data, 'output.data'), true), format: (string) data_get($data, 'format'), @@ -25,6 +35,7 @@ public static function fromResponse(array $data): self size: (int) data_get($data, 'size'), width: (int) data_get($data, 'width'), height: (int) data_get($data, 'height'), + psnr: $psnr === null ? null : (float) $psnr, ); } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index c9ed9dd..f51f276 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -30,7 +30,7 @@ function client(Factory $http, Closure|string|null $token = 'test-token', string } test('convert posts the base64 envelope and returns a decoded ImageResult', function () { - $http = fakeHttp(['*/v1/convert' => Factory::response(fakeTransformResponse())]); + $http = fakeHttp(['*/v1/convert' => Factory::response(fakeTransformResponse(overrides: ['psnr' => 41.27]))]); $result = client($http)->convert(Images::png(), ImageFormat::Jpg); @@ -38,7 +38,8 @@ function client(Factory $http, Closure|string|null $token = 'test-token', string ->and($result->format)->toBe(ImageFormat::Jpg->value) ->and($result->mimeType)->toBe('image/jpeg') ->and($result->width)->toBe(1280) - ->and($result->height)->toBe(720); + ->and($result->height)->toBe(720) + ->and($result->psnr)->toBe(41.27); $http->assertSent(function (Request $request) { return $request->url() === 'https://glimpseimg.com/api/v1/convert' @@ -49,6 +50,25 @@ function client(Factory $http, Closure|string|null $token = 'test-token', string }); }); +test('a null psnr in the response stays null', function () { + $http = fakeHttp(['*/v1/optimize' => Factory::response(fakeTransformResponse(overrides: ['psnr' => null]))]); + + expect(client($http)->optimize(Images::png())->psnr)->toBeNull(); +}); + +test('a response without a psnr field maps to a null psnr', function () { + // Resize and thumbnail responses do not carry the field at all. + $http = fakeHttp(['*/v1/resize' => Factory::response(fakeTransformResponse())]); + + expect(client($http)->resize(Images::png(), width: 800)->psnr)->toBeNull(); +}); + +test('an integer psnr in the response is cast to float', function () { + $http = fakeHttp(['*/v1/optimize' => Factory::response(fakeTransformResponse(overrides: ['psnr' => 42]))]); + + expect(client($http)->optimize(Images::png())->psnr)->toBe(42.0); +}); + test('convert sends optimize and quality when given', function () { $http = fakeHttp(['*/v1/convert' => Factory::response(fakeTransformResponse())]); diff --git a/tests/Pest.php b/tests/Pest.php index ccab4f9..5579e9f 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -68,11 +68,12 @@ function fakeInfoResponse(array $overrides = []): array /** * A canned successful transform-endpoint response envelope. * + * @param array $overrides * @return array{data: array} */ -function fakeTransformResponse(string $format = 'jpg', string $mimeType = 'image/jpeg'): array +function fakeTransformResponse(string $format = 'jpg', string $mimeType = 'image/jpeg', array $overrides = []): array { - return ['data' => [ + return ['data' => $overrides + [ 'output' => ['type' => 'BASE64', 'data' => Images::JPG_BASE64], 'format' => $format, 'mime_type' => $mimeType, From c9d3714bf635867fa047ab4d59b7f5c30fb68a2a Mon Sep 17 00:00:00 2001 From: Mathias Grimm Date: Wed, 22 Jul 2026 10:33:25 -0300 Subject: [PATCH 2/2] Lock six-argument ImageResult construction Adds a test asserting the pre-3.1 six-argument constructor still works and defaults psnr to null, so the additive parameter cannot silently become a breaking change. Co-Authored-By: Claude Fable 5 --- tests/ClientTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/ClientTest.php b/tests/ClientTest.php index f51f276..86a0f0a 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -8,6 +8,7 @@ use MathiasGrimm\GlimpsePhp\ForbiddenException; use MathiasGrimm\GlimpsePhp\ImageFormat; use MathiasGrimm\GlimpsePhp\ImageInfo; +use MathiasGrimm\GlimpsePhp\ImageResult; use MathiasGrimm\GlimpsePhp\RateLimitException; use MathiasGrimm\GlimpsePhp\SizeEstimate; use MathiasGrimm\GlimpsePhp\Tests\Fixtures\Images; @@ -69,6 +70,14 @@ function client(Factory $http, Closure|string|null $token = 'test-token', string expect(client($http)->optimize(Images::png())->psnr)->toBe(42.0); }); +test('the six-argument constructor stays valid and defaults psnr to null', function () { + // Locks backward compatibility: callers built against pre-3.1 must keep + // working without passing psnr. + $result = new ImageResult('bytes', 'jpg', 'image/jpeg', 100, 20, 10); + + expect($result->psnr)->toBeNull(); +}); + test('convert sends optimize and quality when given', function () { $http = fakeHttp(['*/v1/convert' => Factory::response(fakeTransformResponse())]);