1010
1111ENV [ "VIPS_WARNING" ] = "0" # disable libvips warnings
1212
13- require "dhash-vips" unless RUBY_ENGINE == "jruby"
1413require "vips"
1514require "mini_magick"
1615
16+ # Faithful port of DHashVips::IDHash, inlined to avoid the upstream gem's
17+ # fragile install-time test (which breaks on Ruby 4.0) and to make
18+ # similarity assertions available on JRuby as well.
19+ module PerceptualHash
20+ module_function
21+
22+ SIZE = 8
23+
24+ def fingerprint ( path )
25+ image = Vips ::Image . thumbnail ( path , SIZE , height : SIZE , size : :force )
26+ image = image . flatten ( background : 255 ) if image . has_alpha?
27+ image = image . colourspace ( "b-w" ) [ 0 ]
28+ rows = image . to_a . map ( &:flatten )
29+ d1 , i1 = signs_and_intensities ( rows )
30+ d2 , i2 = signs_and_intensities ( rows . transpose )
31+ ( ( ( ( ( i1 << SIZE * SIZE ) | i2 ) << SIZE * SIZE ) | d1 ) << SIZE * SIZE ) | d2
32+ end
33+
34+ def distance ( a , b )
35+ ( ( a ^ b ) & ( ( a | b ) >> 2 * SIZE * SIZE ) ) . to_s ( 2 ) . count ( "1" )
36+ end
37+
38+ def signs_and_intensities ( matrix )
39+ differences = matrix . zip ( matrix . rotate ( 1 ) ) . flat_map do |r1 , r2 |
40+ r1 . zip ( r2 ) . map { |a , b | a - b }
41+ end
42+ threshold = median ( differences . map ( &:abs ) . sort )
43+ signs = differences . inject ( 0 ) { |bits , d | ( bits << 1 ) | ( d < 0 ? 1 : 0 ) }
44+ intensities = differences . inject ( 0 ) { |bits , d | ( bits << 1 ) | ( d . abs >= threshold ? 1 : 0 ) }
45+ [ signs , intensities ]
46+ end
47+
48+ def median ( sorted )
49+ h = sorted . size / 2
50+ return sorted [ h ] if sorted [ h ] != sorted [ h - 1 ]
51+ right = sorted . dup
52+ left = right . shift ( h )
53+ right . shift if right . size > left . size
54+ return right . first if left . last != right . first
55+ return right . uniq [ 1 ] if left . count ( left . last ) > right . count ( right . first )
56+ left . last
57+ end
58+ end
59+
1760class Minitest ::Test
1861 def fixture_image ( name )
1962 File . open ( "test/fixtures/#{ name } " , "rb" )
@@ -27,12 +70,10 @@ def copy_to_tempfile(io, extension = nil)
2770 end
2871
2972 def assert_similar ( image1 , image2 )
30- skip "dhash-vips not available on JRuby" if RUBY_ENGINE == "jruby"
3173 assert_operator distance ( image1 , image2 ) , :<= , 3
3274 end
3375
3476 def refute_similar ( image1 , image2 )
35- skip "dhash-vips not available on JRuby" if RUBY_ENGINE == "jruby"
3677 assert_operator distance ( image1 , image2 ) , :> , 3
3778 end
3879
@@ -47,10 +88,10 @@ def assert_type(type, file)
4788 private
4889
4990 def distance ( image1 , image2 )
50- hash1 = DHashVips :: IDHash . fingerprint ( image1 . path )
51- hash2 = DHashVips :: IDHash . fingerprint ( image2 . path )
52-
53- DHashVips :: IDHash . distance hash1 , hash2
91+ PerceptualHash . distance (
92+ PerceptualHash . fingerprint ( image1 . path ) ,
93+ PerceptualHash . fingerprint ( image2 . path ) ,
94+ )
5495 end
5596end
5697
0 commit comments