5151DEFAULT_BACKEND = "agg"
5252
5353SUPPORTED_FORMATS = {"html" , "json" , "basic-html" }
54+ SUPPORTED_HASH_METHODS = {
55+ "sha256" ,
56+ "ahash" ,
57+ "phash" ,
58+ "phash_simple" ,
59+ "dhash" ,
60+ "dhash_vertical" ,
61+ "whash" ,
62+ "colorhash" ,
63+ "crop_resistant_hash" ,
64+ }
5465
5566SHAPE_MISMATCH_ERROR = """Error: Image dimensions did not match.
5667 Expected shape: {expected_shape}
@@ -83,6 +94,58 @@ def _hash_file(in_stream):
8394 return hasher .hexdigest ()
8495
8596
97+ def _normalize_hash_method (hash_method ):
98+ if hash_method is None :
99+ return "sha256"
100+ method = str (hash_method ).lower ()
101+ if method not in SUPPORTED_HASH_METHODS :
102+ raise ValueError (
103+ f"Unsupported hash method '{ hash_method } '. "
104+ f"Supported methods are: { sorted (SUPPORTED_HASH_METHODS )} ."
105+ )
106+ return method
107+
108+
109+ def _compute_imagehash (hash_method , in_stream ):
110+ try :
111+ import imagehash
112+ except ImportError as exc :
113+ raise ImportError (
114+ "Hash method requires the 'ImageHash' package. "
115+ "Install pytest-mpl with the 'hashes' extra or add ImageHash to your dependencies."
116+ ) from exc
117+
118+ in_stream .seek (0 )
119+ try :
120+ from PIL import Image
121+ except ImportError as exc :
122+ raise ImportError (
123+ "Hash method requires Pillow to load image data."
124+ ) from exc
125+
126+ image = Image .open (in_stream )
127+
128+ methods = {
129+ "ahash" : imagehash .average_hash ,
130+ "phash" : imagehash .phash ,
131+ "phash_simple" : imagehash .phash_simple ,
132+ "dhash" : imagehash .dhash ,
133+ "dhash_vertical" : imagehash .dhash_vertical ,
134+ "whash" : imagehash .whash ,
135+ "colorhash" : imagehash .colorhash ,
136+ "crop_resistant_hash" : imagehash .crop_resistant_hash ,
137+ }
138+ if hash_method not in methods :
139+ raise ValueError (f"Unsupported imagehash method '{ hash_method } '." )
140+
141+ try :
142+ return str (methods [hash_method ](image ))
143+ except ImportError as exc :
144+ raise ImportError (
145+ f"Hash method '{ hash_method } ' requires extra optional dependencies."
146+ ) from exc
147+
148+
86149def pathify (path ):
87150 """
88151 Remove non-path safe characters.
@@ -166,6 +229,11 @@ def pytest_addoption(parser):
166229 group .addoption (f"--{ option } " , help = msg , action = "store" )
167230 parser .addini (option , help = msg )
168231
232+ msg = "hash method to use for hash comparison and generation"
233+ option = "mpl-hash-method"
234+ group .addoption (f"--{ option } " , help = msg , action = "store" )
235+ parser .addini (option , help = msg )
236+
169237 msg = (
170238 "Generate a summary report of any failed tests"
171239 ", in --mpl-results-path. The type of the report should be "
@@ -251,6 +319,7 @@ def get_cli_or_ini(name, default=None):
251319
252320 hash_library = get_cli_or_ini ("mpl-hash-library" )
253321 _hash_library_from_cli = bool (config .getoption ("--mpl-hash-library" )) # for backwards compatibility
322+ hash_method = _normalize_hash_method (get_cli_or_ini ("mpl-hash-method" , "sha256" ))
254323
255324 default_tolerance = get_cli_or_ini ("mpl-default-tolerance" , DEFAULT_TOLERANCE )
256325 if isinstance (default_tolerance , str ):
@@ -310,6 +379,7 @@ def get_cli_or_ini(name, default=None):
310379 baseline_relative_dir = baseline_relative_dir ,
311380 generate_dir = generate_dir ,
312381 hash_library = hash_library ,
382+ hash_method = hash_method ,
313383 generate_hash_library = generate_hash_lib ,
314384 generate_summary = generate_summary ,
315385 results_always = results_always ,
@@ -372,6 +442,7 @@ def __init__(
372442 baseline_relative_dir = None ,
373443 generate_dir = None ,
374444 hash_library = None ,
445+ hash_method = "sha256" ,
375446 generate_hash_library = None ,
376447 generate_summary = None ,
377448 results_always = False ,
@@ -388,6 +459,7 @@ def __init__(
388459 self .generate_dir = path_is_not_none (generate_dir )
389460 self .results_dir = None
390461 self .hash_library = path_is_not_none (hash_library )
462+ self .hash_method = _normalize_hash_method (hash_method )
391463 self ._hash_library_from_cli = _hash_library_from_cli # for backwards compatibility
392464 self .generate_hash_library = path_is_not_none (generate_hash_library )
393465 if generate_summary :
@@ -569,13 +641,24 @@ def generate_baseline_image(self, item, fig):
569641
570642 def generate_image_hash (self , item , fig ):
571643 """
572- For a `matplotlib.figure.Figure`, returns the SHA256 hash as a hexadecimal
644+ For a `matplotlib.figure.Figure`, returns the hash as a hexadecimal
573645 string.
574646 """
647+ compare = get_compare (item )
648+ hash_method = _normalize_hash_method (compare .kwargs .get ('hash_method' , self .hash_method ))
649+ ext = self ._file_extension (item )
650+ if hash_method != "sha256" and ext not in RASTER_IMAGE_FORMATS :
651+ raise ValueError (
652+ f"Hash method '{ hash_method } ' only supports raster formats { RASTER_IMAGE_FORMATS } . "
653+ f"Got format '{ ext } '."
654+ )
575655
576656 imgdata = io .BytesIO ()
577657 self .save_figure (item , fig , imgdata )
578- out = _hash_file (imgdata )
658+ if hash_method == "sha256" :
659+ out = _hash_file (imgdata )
660+ else :
661+ out = _compute_imagehash (hash_method , imgdata )
579662 imgdata .close ()
580663
581664 close_mpl_figure (fig )
0 commit comments