diff --git a/.travis.yml b/.travis.yml index 0457012..b312f7f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,10 @@ language: php php: - - 5.4 - - 5.5 - 5.6 - 7.0 - 7.1 - 7.2 - - hhvm sudo: false @@ -20,4 +17,4 @@ script: - ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml after_success: - - sh -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then php vendor/bin/coveralls -v; fi;' + - php vendor/bin/coveralls -v diff --git a/composer.json b/composer.json index 56eab7d..a1abb25 100644 --- a/composer.json +++ b/composer.json @@ -15,14 +15,16 @@ ], "require": { "php": ">=5.4.0", - "ext-gd": "*" + "ext-gd": "*", + "intervention/image": "^2.4" }, "require-dev": { - "phpunit/phpunit": "^4.0|^5.0", - "satooshi/php-coveralls": "^0.6" + "phpunit/phpunit": "^4.0|^5.0|^6.0|^7.0", + "satooshi/php-coveralls": "^2.0" }, "suggest": { - "ext-gmp": "It can speed up the image hash." + "ext-gmp": "GD or ImageMagick is required", + "ext-imagick": "GD or ImageMagick is required" }, "autoload": { "psr-4": { diff --git a/src/ImageHash.php b/src/ImageHash.php index 1bed620..daa3607 100644 --- a/src/ImageHash.php +++ b/src/ImageHash.php @@ -1,8 +1,9 @@ implementation = $implementation ?: new DifferenceHash; $this->mode = $mode; + $this->driver = $driver ?: $this->defaultDriver(); } /** - * Calculate a perceptual hash of an image file. + * Calculate a perceptual hash of an image. * - * @param mixed $resource GD2 resource or filename + * @param mixed $image * @return int */ - public function hash($resource) + public function hash($image) { - $destroy = false; + $image = $this->driver->make($image); - if (! is_resource($resource)) { - $resource = $this->loadImageResource($resource); - $destroy = true; - } - - $hash = $this->implementation->hash($resource); - - if ($destroy) { - $this->destroyResource($resource); - } + $hash = $this->implementation->hash($image); return $this->formatHash($hash); } @@ -67,18 +69,13 @@ public function hash($resource) /** * Calculate a perceptual hash of an image string. * + * @deprecated * @param mixed $data Image data * @return string */ public function hashFromString($data) { - $resource = $this->createResource($data); - - $hash = $this->implementation->hash($resource); - - $this->destroyResource($resource); - - return $this->formatHash($hash); + return $this->hash($data); } /** @@ -107,7 +104,7 @@ public function distance($hash1, $hash2) { if (extension_loaded('gmp')) { if ($this->mode === self::HEXADECIMAL) { - $dh = gmp_hamdist('0x'.$hash1, '0x'.$hash2); + $dh = gmp_hamdist('0x' . $hash1, '0x' . $hash2); } else { $dh = gmp_hamdist($hash1, $hash2); } @@ -137,7 +134,7 @@ public function distance($hash1, $hash2) */ public function hexdec($hex) { - if (strlen($hex) == 16 && hexdec($hex[0]) > 8) { + if (strlen($hex) === 16 && hexdec($hex[0]) > 8) { list($higher, $lower) = array_values(unpack('N2', hex2bin($hex))); return $higher << 32 | $lower; } @@ -146,43 +143,12 @@ public function hexdec($hex) } /** - * Get a GD2 resource from file. - * - * @param string $file - * @return resource - */ - protected function loadImageResource($file) - { - try { - return $this->createResource(file_get_contents($file)); - } catch (Exception $e) { - throw new InvalidArgumentException("Unable to load file: $file"); - } - } - - /** - * Get a GD2 resource from string. - * * @param string $data - * @return resource + * @return Image */ protected function createResource($data) { - try { - return imagecreatefromstring($data); - } catch (Exception $e) { - throw new InvalidArgumentException('Unable to create GD2 resource'); - } - } - - /** - * Destroy GD2 resource. - * - * @param resource $resource - */ - protected function destroyResource($resource) - { - imagedestroy($resource); + return $this->driver->make($data); } /** @@ -195,4 +161,21 @@ protected function formatHash($hash) { return $this->mode === static::HEXADECIMAL ? dechex($hash) : $hash; } + + /** + * @return ImageManager + * @throws RuntimeException + */ + protected function defaultDriver() + { + if (extension_loaded('gd')) { + return new ImageManager(['driver' => 'gd']); + } + + if (extension_loaded('imagick')) { + return new ImageManager(['driver' => 'imagick']); + } + + throw new RuntimeException('Please install GD or ImageMagick'); + } } diff --git a/src/Implementation.php b/src/Implementation.php index b3d8897..0aa9c06 100644 --- a/src/Implementation.php +++ b/src/Implementation.php @@ -1,12 +1,14 @@ resize(static::SIZE, static::SIZE); // Create an array of greyscale pixel values. $pixels = []; for ($y = 0; $y < static::SIZE; $y++) { for ($x = 0; $x < static::SIZE; $x++) { - $rgb = imagecolorsforindex($resized, imagecolorat($resized, $x, $y)); - $pixels[] = floor(($rgb['red'] + $rgb['green'] + $rgb['blue']) / 3); + $rgb = $resized->pickColor($x, $y); + $pixels[] = floor(($rgb[0] + $rgb[1] + $rgb[2]) / 3); } } - // Free up memory. - imagedestroy($resized); - // Get the average pixel value. $average = floor(array_sum($pixels) / count($pixels)); diff --git a/src/Implementations/DifferenceHash.php b/src/Implementations/DifferenceHash.php index 4f51abb..66250bf 100644 --- a/src/Implementations/DifferenceHash.php +++ b/src/Implementations/DifferenceHash.php @@ -1,5 +1,6 @@ resize($width, $height); $hash = 0; $one = 1; - for ($y = 0; $y < $heigth; $y++) { + for ($y = 0; $y < $height; $y++) { // Get the pixel value for the leftmost pixel. - $rgb = imagecolorsforindex($resized, imagecolorat($resized, 0, $y)); - $left = floor(($rgb['red'] + $rgb['green'] + $rgb['blue']) / 3); + $rgb = $resized->pickColor(0, $y); + $left = floor(($rgb[0] + $rgb[1] + $rgb[2]) / 3); for ($x = 1; $x < $width; $x++) { // Get the pixel value for each pixel starting from position 1. - $rgb = imagecolorsforindex($resized, imagecolorat($resized, $x, $y)); - $right = floor(($rgb['red'] + $rgb['green'] + $rgb['blue']) / 3); + $rgb = $resized->pickColor($x, $y); + $right = floor(($rgb[0] + $rgb[1] + $rgb[2]) / 3); // Each hash bit is set based on whether the left pixel is brighter than the right pixel. // http://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html @@ -43,9 +43,6 @@ public function hash($resource) } } - // Free up memory. - imagedestroy($resized); - return $hash; } } diff --git a/src/Implementations/PerceptualHash.php b/src/Implementations/PerceptualHash.php index 4267e5c..8c4e7cb 100644 --- a/src/Implementations/PerceptualHash.php +++ b/src/Implementations/PerceptualHash.php @@ -1,5 +1,6 @@ resize(static::SIZE, static::SIZE); // Get luma value (YCbCr) from RGB colors and calculate the DCT for each row. $matrix = []; $row = []; $rows = []; $col = []; - $cols = []; for ($y = 0; $y < static::SIZE; $y++) { for ($x = 0; $x < static::SIZE; $x++) { - $rgb = imagecolorsforindex($resized, imagecolorat($resized, $x, $y)); - $row[$x] = floor(($rgb['red'] * 0.299) + ($rgb['green'] * 0.587) + ($rgb['blue'] * 0.114)); + $rgb = $resized->pickColor($x, $y); + $row[$x] = floor(($rgb[0] * 0.299) + ($rgb[1] * 0.587) + ($rgb[2] * 0.114)); } $rows[$y] = $this->DCT1D($row); } - // Free up memory. - imagedestroy($resized); - // Calculate the DCT for each column. for ($x = 0; $x < static::SIZE; $x++) { for ($y = 0; $y < static::SIZE; $y++) { @@ -68,6 +64,7 @@ public function hash($resource) * Perform a 1 dimension Discrete Cosine Transformation. * * @param array $pixels + * @return array */ protected function DCT1D(array $pixels) { @@ -77,12 +74,12 @@ protected function DCT1D(array $pixels) for ($i = 0; $i < $size; $i++) { $sum = 0; for ($j = 0; $j < $size; $j++) { - $sum += $pixels[$j] * cos($i * pi() * ($j + 0.5) / ($size)); + $sum += $pixels[$j] * cos($i * pi() * ($j + 0.5) / $size); } $sum *= sqrt(2 / $size); - if ($i == 0) { + if ($i === 0) { $sum *= 1 / sqrt(2); } @@ -101,7 +98,7 @@ protected function DCT1D(array $pixels) protected function median(array $pixels) { sort($pixels, SORT_NUMERIC); - $middle = floor(count($pixels) / 2); + $middle = (int) floor(count($pixels) / 2); if (count($pixels) % 2) { $median = $pixels[$middle]; diff --git a/tests/ImageHashTest.php b/tests/ImageHashTest.php index 0248984..817aece 100644 --- a/tests/ImageHashTest.php +++ b/tests/ImageHashTest.php @@ -1,5 +1,6 @@ imageHash = new ImageHash(); } - public function testHashStringInvalidFile() + public function testHashInvalidFile() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(NotReadableException::class); - $this->imageHash->hashFromString('nonImageString'); - } - - public function testHashStringSameAsFile() - { - $path = 'tests/images/forest/forest-low.jpg'; - - $this->assertSame($this->imageHash->hash($path), $this->imageHash->hashFromString(file_get_contents($path))); + $this->imageHash->hash('nonImageString'); } public function testHexdecForNegativeIntegers() diff --git a/tests/ImageTest.php b/tests/ImageTest.php index 4dad507..55d3896 100644 --- a/tests/ImageTest.php +++ b/tests/ImageTest.php @@ -19,9 +19,9 @@ class ImageTest extends TestCase public static function setUpBeforeClass() { if (extension_loaded('gmp')) { - echo "INFO: gmp extension loaded" . PHP_EOL; + echo 'INFO: gmp extension loaded' . PHP_EOL; } else { - echo "INFO: gmp extension not loaded" . PHP_EOL; + echo 'INFO: gmp extension not loaded' . PHP_EOL; } } @@ -39,7 +39,7 @@ public function testEqualHashes() foreach ($this->hashers as $hasher) { $score = 0; $imageHash = new ImageHash($hasher); - $images = glob('tests/images/forest/*'); + $images = glob(__DIR__ . '/images/forest/*'); $hashes = []; foreach ($images as $image) { @@ -62,7 +62,7 @@ public function testEqualHashes() } } - echo "[" . get_class($hasher) . "] Total score: $score" . PHP_EOL; + echo "[" . get_class($hasher) . "] Total score: $score" . PHP_EOL; } } @@ -71,7 +71,7 @@ public function testDifferentHashes() foreach ($this->hashers as $hasher) { $score = 0; $imageHash = new ImageHash($hasher); - $images = glob('tests/images/office/*'); + $images = glob(__DIR__ . '/images/office/*'); $hashes = []; foreach ($images as $image) { @@ -94,7 +94,7 @@ public function testDifferentHashes() } } - echo "[" . get_class($hasher) . "] Total score: $score" . PHP_EOL; + echo "[" . get_class($hasher) . "] Total score: $score" . PHP_EOL; } } @@ -102,7 +102,7 @@ public function testCompareEqual() { foreach ($this->hashers as $hasher) { $imageHash = new ImageHash($hasher); - $images = glob('tests/images/forest/*'); + $images = glob(__DIR__ . '/images/forest/*'); foreach ($images as $image) { foreach ($images as $target) { @@ -121,7 +121,7 @@ public function testCompareDifferent() { foreach ($this->hashers as $hasher) { $imageHash = new ImageHash($hasher); - $images = glob('tests/images/office/*'); + $images = glob(__DIR__ . '/images/office/*'); foreach ($images as $image) { foreach ($images as $target) { @@ -139,7 +139,7 @@ public function testCompareDifferent() public function testHexadecimalMode() { $imageHash = new ImageHash(new DifferenceHash(), ImageHash::HEXADECIMAL); - $images = glob('tests/images/office/*'); + $images = glob(__DIR__ . '/images/office/*'); $hash = $imageHash->hash($images[0]); $this->assertTrue(ctype_xdigit($hash), $hash); @@ -149,10 +149,10 @@ public function testHexadecimalMode() public function testDecimalMode() { $imageHash = new ImageHash(new DifferenceHash(), ImageHash::DECIMAL); - $images = glob('tests/images/office/*'); + $images = glob(__DIR__ . '/images/office/*'); $hash = $imageHash->hash($images[0]); - $this->assertTrue(is_int($hash), $hash); + $this->assertInternalType('int', $hash); $this->assertEquals(0, $imageHash->distance($hash, $hash)); } }