22This module contains the supported hashing kernel implementations.
33
44"""
5- from PIL import Image
6- from abc import ABC , abstractmethod
75import hashlib
6+ from abc import ABC , abstractmethod
7+
88import imagehash
9+ from PIL import Image
10+
11+ #: The default hamming distance bit tolerance for "similar" imagehash hashes.
12+ DEFAULT_HAMMING_TOLERANCE = 2
913
14+ #: The default imagehash hash size (N), resulting in a hash of N**2 bits.
15+ DEFAULT_HASH_SIZE = 16
1016
17+ #: Registered kernel names.
1118KERNEL_SHA256 = "sha256"
1219KERNEL_PHASH = "phash"
1320
1421__all__ = [
22+ "DEFAULT_HAMMING_TOLERANCE" ,
23+ "DEFAULT_HASH_SIZE" ,
1524 "KERNEL_PHASH" ,
1625 "KERNEL_SHA256" ,
1726 "KernelPHash" ,
2231
2332class Kernel (ABC ):
2433 """
25- Kernel abstract base class (ABC) which defines a common kernel API.
34+ Kernel abstract base class (ABC) which defines a simple common kernel API.
2635
2736 """
2837
2938 def __init__ (self , plugin ):
39+ # Containment (read-only) of the plugin allows the kernel to cherry-pick state that it requires
3040 self ._plugin = plugin
3141
42+ @abstractmethod
43+ def equivalent_hash (self , actual , expected , marker = None ):
44+ """
45+ Determine whether the kernel considers the provided actual and
46+ expected hashes as similar.
47+
48+ Parameters
49+ ----------
50+ actual : str
51+ The hash of the test image.
52+ expected : str
53+ The hash of the baseline image.
54+ marker : pytest.Mark
55+ The test marker, which may contain kwarg options to be
56+ applied to the equivalence test.
57+
58+ Returns
59+ -------
60+ bool
61+ Whether the actual and expected hashes are deemed similar.
62+
63+ """
64+
3265 @abstractmethod
3366 def generate_hash (self , buffer ):
3467 """
@@ -47,28 +80,22 @@ def generate_hash(self, buffer):
4780
4881 """
4982
50- @abstractmethod
51- def equivalent_hash (self , actual , expected , marker = None ):
83+ def update_status (self , message ):
5284 """
53- Determine whether the kernel considers the provided actual and
54- expected hashes as similar.
85+ Append the kernel status message to the provided message.
5586
5687 Parameters
5788 ----------
58- actual : str
59- The hash of the test image.
60- expected : str
61- The hash of the baseline image.
62- marker : pytest.Mark
63- The test marker, which may contain kwarg options to be
64- applied to the equivalence test.
89+ message : str
90+ The existing status message.
6591
6692 Returns
6793 -------
68- bool
69- Whether the actual and expected hashes are deemed similar .
94+ str
95+ The updated status message .
7096
7197 """
98+ return message
7299
73100 def update_summary (self , summary ):
74101 """
@@ -81,7 +108,7 @@ def update_summary(self, summary):
81108 Returns
82109 -------
83110 dict
84- The image comparison summary.
111+ The updated image comparison summary.
85112
86113 """
87114 return summary
@@ -104,23 +131,34 @@ def __init__(self, plugin):
104131 # py.test marker kwarg
105132 self .option = "hamming_tolerance"
106133 # value may be overridden by py.test marker kwarg
107- self .hamming_tolerance = self ._plugin .hamming_tolerance
134+ self .hamming_tolerance = self ._plugin .hamming_tolerance or DEFAULT_HAMMING_TOLERANCE
108135 # keep state of hash hamming distance (whole number) result
109136 self .hamming_distance = None
110-
111- def generate_hash (self , buffer ):
112- buffer .seek (0 )
113- data = Image .open (buffer )
114- phash = imagehash .phash (data , hash_size = self .hash_size )
115- return str (phash )
137+ # keep state of equivalence result
138+ self .equivalent = None
116139
117140 def equivalent_hash (self , actual , expected , marker = None ):
118141 if marker :
119142 self .hamming_tolerance = int (marker .kwargs .get (self .option ))
120143 actual = imagehash .hex_to_hash (actual )
121144 expected = imagehash .hex_to_hash (expected )
122145 self .hamming_distance = actual - expected
123- return self .hamming_distance <= self .hamming_tolerance
146+ self .equivalent = self .hamming_distance <= self .hamming_tolerance
147+ return self .equivalent
148+
149+ def generate_hash (self , buffer ):
150+ buffer .seek (0 )
151+ data = Image .open (buffer )
152+ phash = imagehash .phash (data , hash_size = self .hash_size )
153+ return str (phash )
154+
155+ def update_status (self , message ):
156+ result = str () if message is None else str (message )
157+ if self .equivalent is False :
158+ msg = (f"Hash hamming distance of { self .hamming_distance } bits > "
159+ f"hamming tolerance of { self .hamming_tolerance } bits." )
160+ result = f"{ message } { msg } " if len (result ) else msg
161+ return result
124162
125163 def update_summary (self , summary ):
126164 summary ["hamming_distance" ] = self .hamming_distance
@@ -136,16 +174,16 @@ class KernelSHA256(Kernel):
136174
137175 name = KERNEL_SHA256
138176
177+ def equivalent_hash (self , actual , expected , marker = None ):
178+ return actual == expected
179+
139180 def generate_hash (self , buffer ):
140181 buffer .seek (0 )
141182 data = buffer .read ()
142183 hasher = hashlib .sha256 ()
143184 hasher .update (data )
144185 return hasher .hexdigest ()
145186
146- def equivalent_hash (self , actual , expected , marker = None ):
147- return actual == expected
148-
149187
150188#: Registry of available hashing kernel factories.
151189kernel_factory = {
0 commit comments