getImageWidth(), $image->getImageHeight() ) : array( imagesx( $image ), imagesy( $image ) ); } /** * Some pixels of one row as RGBA bytes (0 to 255). * * @param mixed $image An Imagick picture or a GD image. * @param string $backend imagick or gd. * @param int $x The first column. * @param int $y The row. * @param int $n How many pixels. * @return array R, G, B and A values, pixel after pixel. */ private static function row( $image, string $backend, int $x, int $y, int $n ): array { if ( 'imagick' === $backend ) { return $image->exportImagePixels( $x, $y, $n, 1, 'RGBA', \Imagick::PIXEL_CHAR ); } $out = array(); for ( $i = $x; $i < $x + $n; ++$i ) { $c = imagecolorsforindex( $image, imagecolorat( $image, $i, $y ) ); $out[] = $c['red']; $out[] = $c['green']; $out[] = $c['blue']; $out[] = 255 - (int) round( $c['alpha'] * 255 / 127 ); } return $out; } /** * Compare a copy with its source: every pixel for lossless copies; for others a sampled luma SSIM and RGB error * against the limits of the chosen fidelity. * * @param mixed $a The source, decoded (Imagick or GD). * @param mixed $b The copy, decoded the same way. * @param string $backend imagick or gd. * @param string $fidelity lossless or the photo fidelity of the recipe. * @return array The method used and its measurements. * @throws \RuntimeException With a reason code as its message when the step cannot be done. */ public static function compare( $a, $b, string $backend, string $fidelity ): array { list( $w, $h ) = self::dimensions( $a, $backend ); if ( array( $w, $h ) !== self::dimensions( $b, $backend ) ) { throw new \RuntimeException( 'candidate_dimensions_or_mime' ); } if ( 'lossless' === $fidelity ) { for ( $y = 0; $y < $h; ++$y ) { if ( self::row( $a, $backend, 0, $y, $w ) !== self::row( $b, $backend, 0, $y, $w ) ) { throw new \RuntimeException( 'pixel_or_source_mismatch' ); } } return array( 'method' => 'exact decoded RGBA', 'exact' => true, 'pixels_checked' => $w * $h, 'passed' => true, ); } // Sample 8x8 patches on a regular grid plus edges. Maximum approximately 70k source pixels. $step = 8 * max( 1, (int) ceil( sqrt( $w * $h / 65536 ) ) ); $xs = array_unique( array_merge( range( 0, max( 0, $w - 8 ), $step ), array( max( 0, $w - 8 ) ) ) ); $ys = array_unique( array_merge( range( 0, max( 0, $h - 8 ), $step ), array( max( 0, $h - 8 ) ) ) ); $sum = 0.0; $worst = 1.0; $error = 0.0; $pixels = 0; $patches = 0; foreach ( $ys as $y ) { foreach ( $xs as $x ) { $sx = $sy = $sxx = $syy = $sxy = 0.0; $n = 0; for ( $yy = $y; $yy < min( $h, $y + 8 ); ++$yy ) { $ra = self::row( $a, $backend, $x, $yy, min( 8, $w - $x ) ); $rb = self::row( $b, $backend, $x, $yy, min( 8, $w - $x ) ); for ( $i = 0; $i < count( $ra ); $i += 4 ) { if ( $ra[ $i + 3 ] !== $rb[ $i + 3 ] ) { throw new \RuntimeException( 'alpha_changed' ); } $la = 0.2126 * $ra[ $i ] + 0.7152 * $ra[ $i + 1 ] + 0.0722 * $ra[ $i + 2 ]; $lb = 0.2126 * $rb[ $i ] + 0.7152 * $rb[ $i + 1 ] + 0.0722 * $rb[ $i + 2 ]; $sx += $la; $sy += $lb; $sxx += $la * $la; $syy += $lb * $lb; $sxy += $la * $lb; $error += abs( $ra[ $i ] - $rb[ $i ] ) + abs( $ra[ $i + 1 ] - $rb[ $i + 1 ] ) + abs( $ra[ $i + 2 ] - $rb[ $i + 2 ] ); ++$n; ++$pixels; } } $mx = $sx / $n; $my = $sy / $n; $vx = max( 0, $sxx / $n - $mx * $mx ); $vy = max( 0, $syy / $n - $my * $my ); $cov = $sxy / $n - $mx * $my; $ssim = ( ( 2 * $mx * $my + 6.5025 ) * ( 2 * $cov + 58.5225 ) ) / ( ( $mx * $mx + $my * $my + 6.5025 ) * ( $vx + $vy + 58.5225 ) ); $sum += $ssim; $worst = min( $worst, $ssim ); ++$patches; } } $mean = $sum / max( 1, $patches ); $mae = $error / max( 1, $pixels * 3 ); // Conservative initial gates calibrated against the release fixture corpus, not universal visual guarantees. $limits = 'maximum' === $fidelity ? array( .95, .60, 6.0 ) : array( .98, .75, 3.5 ); return array( 'method' => 'sampled full-resolution 8x8 luma SSIM + RGB MAE v1', 'exact' => false, 'pixels_sampled' => $pixels, 'patches' => $patches, 'ssim' => round( $mean, 6 ), 'worst_patch' => round( $worst, 6 ), 'rgb_mae' => round( $mae, 4 ), 'limits' => $limits, 'passed' => $mean >= $limits[0] && $worst >= $limits[1] && $mae <= $limits[2], ); } }