packages feed

perceptual-hash 0.1.4.5 → 0.1.4.6

raw patch · 12 files changed

+294/−233 lines, 12 filesdep +avifdep ~webpbinary-addedPVP: minor bump suggested

API additions: PVP suggests at least a minor version bump

Dependencies added: avif

Dependency ranges changed: webp

API changes (from Hackage documentation)

+ ForeignHash: foreignFileHash :: FilePath -> IO Word64

Files

CHANGELOG.md view
@@ -1,5 +1,9 @@ # phash +## 0.1.4.6++  * Add support for `.avif` images+ ## 0.1.4.5    * Modest performance improvements
README.md view
@@ -1,9 +1,5 @@ # phash -[![Hackage CI](https://matrix.hackage.haskell.org/api/v2/packages/perceptual-hash/badge)](https://matrix.hackage.haskell.org/package/perceptual-hash)-[![Hackage](https://img.shields.io/hackage/v/perceptual-hash.svg)](http://hackage.haskell.org/package/perceptual-hash)-[![Dependencies of latest version on Hackage](https://img.shields.io/hackage-deps/v/perceptual-hash.svg)](https://hackage.haskell.org/package/perceptual-hash)- This is a Haskell [library](http://hackage.haskell.org/package/perceptual-hash) to detect (potential) duplicate images. @@ -49,45 +45,99 @@  ### Performance -This library has similar performance to the pHash library for PNG-images.+This library has performs better on WebP, AVIF images and worse on JPEG images compared to the pHash library.  ``` benchmarking fileHash/cat.png-time                 20.67 ms   (20.60 ms .. 20.78 ms)+time                 21.50 ms   (21.23 ms .. 21.70 ms)                      1.000 R²   (0.999 R² .. 1.000 R²)-mean                 20.84 ms   (20.76 ms .. 21.01 ms)-std dev              246.3 μs   (149.5 μs .. 396.7 μs)+mean                 21.54 ms   (21.39 ms .. 21.88 ms)+std dev              525.3 μs   (224.2 μs .. 946.8 μs)  benchmarking fileHash/frog.jpeg-time                 17.82 ms   (17.62 ms .. 18.05 ms)-                     0.999 R²   (0.999 R² .. 1.000 R²)-mean                 17.54 ms   (17.45 ms .. 17.65 ms)-std dev              272.3 μs   (194.9 μs .. 369.3 μs)+time                 21.33 ms   (20.57 ms .. 22.01 ms)+                     0.997 R²   (0.996 R² .. 0.999 R²)+mean                 19.84 ms   (19.53 ms .. 20.24 ms)+std dev              822.7 μs   (654.3 μs .. 1.070 ms)+variance introduced by outliers: 13% (moderately inflated)  benchmarking fileHash/frog.png-time                 12.02 ms   (11.95 ms .. 12.11 ms)-                     1.000 R²   (0.999 R² .. 1.000 R²)-mean                 12.08 ms   (12.04 ms .. 12.13 ms)-std dev              115.0 μs   (90.79 μs .. 144.7 μs)+time                 12.31 ms   (11.88 ms .. 12.66 ms)+                     0.997 R²   (0.994 R² .. 0.999 R²)+mean                 12.46 ms   (12.34 ms .. 12.58 ms)+std dev              314.3 μs   (256.0 μs .. 385.0 μs) +benchmarking fileHash/liz-taylor.webp+time                 69.02 ms   (68.00 ms .. 70.23 ms)+                     0.999 R²   (0.997 R² .. 1.000 R²)+mean                 71.69 ms   (70.28 ms .. 74.98 ms)+std dev              3.688 ms   (1.345 ms .. 6.107 ms)+variance introduced by outliers: 16% (moderately inflated)++benchmarking fileHash/liz-taylor.png+time                 72.32 ms   (70.35 ms .. 74.49 ms)+                     0.998 R²   (0.996 R² .. 1.000 R²)+mean                 73.83 ms   (72.60 ms .. 75.38 ms)+std dev              2.414 ms   (1.655 ms .. 3.614 ms)++benchmarking fileHash/fashion.png+time                 142.0 ms   (131.6 ms .. 153.4 ms)+                     0.997 R²   (0.991 R² .. 1.000 R²)+mean                 145.8 ms   (142.0 ms .. 153.9 ms)+std dev              7.690 ms   (2.694 ms .. 11.43 ms)+variance introduced by outliers: 13% (moderately inflated)++benchmarking fileHash/fashion.avif+time                 172.2 ms   (161.5 ms .. 187.6 ms)+                     0.996 R²   (0.991 R² .. 1.000 R²)+mean                 175.1 ms   (169.3 ms .. 185.3 ms)+std dev              12.16 ms   (5.052 ms .. 18.42 ms)+variance introduced by outliers: 13% (moderately inflated)+ benchmarking foreignHash/cat.png-time                 18.86 ms   (18.79 ms .. 18.97 ms)-                     1.000 R²   (1.000 R² .. 1.000 R²)-mean                 18.92 ms   (18.87 ms .. 18.99 ms)-std dev              149.7 μs   (129.0 μs .. 183.6 μs)+time                 21.06 ms   (20.88 ms .. 21.19 ms)+                     1.000 R²   (0.999 R² .. 1.000 R²)+mean                 20.94 ms   (20.80 ms .. 21.06 ms)+std dev              320.2 μs   (225.5 μs .. 418.3 μs)  benchmarking foreignHash/frog.jpeg-time                 8.533 ms   (8.480 ms .. 8.574 ms)-                     1.000 R²   (0.999 R² .. 1.000 R²)-mean                 8.686 ms   (8.644 ms .. 8.745 ms)-std dev              129.0 μs   (97.99 μs .. 164.8 μs)+time                 9.255 ms   (9.168 ms .. 9.378 ms)+                     0.999 R²   (0.999 R² .. 1.000 R²)+mean                 9.234 ms   (9.201 ms .. 9.273 ms)+std dev              94.68 μs   (79.14 μs .. 120.7 μs)  benchmarking foreignHash/frog.png-time                 9.697 ms   (9.649 ms .. 9.735 ms)+time                 10.37 ms   (10.28 ms .. 10.47 ms)+                     0.999 R²   (0.999 R² .. 1.000 R²)+mean                 10.42 ms   (10.37 ms .. 10.48 ms)+std dev              135.4 μs   (106.5 μs .. 184.9 μs)++benchmarking foreignHash/liz-taylor.webp+time                 368.7 ms   (352.4 ms .. 391.1 ms)+                     1.000 R²   (0.999 R² .. 1.000 R²)+mean                 362.3 ms   (357.6 ms .. 366.4 ms)+std dev              5.133 ms   (1.154 ms .. 6.530 ms)+variance introduced by outliers: 19% (moderately inflated)++benchmarking foreignHash/liz-taylor.png+time                 71.58 ms   (71.12 ms .. 72.03 ms)                      1.000 R²   (1.000 R² .. 1.000 R²)-mean                 9.746 ms   (9.717 ms .. 9.775 ms)-std dev              84.08 μs   (70.14 μs .. 97.91 μs)+mean                 71.84 ms   (71.66 ms .. 72.24 ms)+std dev              502.0 μs   (339.8 μs .. 710.2 μs)++benchmarking foreignHash/fashion.png+time                 162.1 ms   (160.6 ms .. 162.8 ms)+                     1.000 R²   (1.000 R² .. 1.000 R²)+mean                 163.4 ms   (162.9 ms .. 164.3 ms)+std dev              990.0 μs   (544.6 μs .. 1.476 ms)+variance introduced by outliers: 12% (moderately inflated)++benchmarking foreignHash/fashion.avif+time                 705.5 ms   (669.6 ms .. 757.2 ms)+                     0.999 R²   (0.998 R² .. 1.000 R²)+mean                 716.4 ms   (710.3 ms .. 728.3 ms)+std dev              11.54 ms   (1.129 ms .. 13.84 ms)+variance introduced by outliers: 19% (moderately inflated) ```  ### Foreign Library
− app/Parallel.cpphs
@@ -1,54 +0,0 @@-{-# LANGUAGE ScopedTypeVariables #-}--module Parallel ( pathMaps ) where--import           Control.Concurrent.STM      (atomically)-import           Control.Concurrent.STM.TVar (TVar, modifyTVar', newTVarIO, readTVarIO)-import           Data.Functor                (($>))-import           Data.List.NonEmpty          (NonEmpty (..), (<|))-import qualified Data.Map                    as M-import           Data.Word                   (Word64)-import           PerceptualHash              (fileHash)-import           System.Directory.Parallel   (parTraverseAll)-import           System.FilePath             (takeExtension)--imgExtension :: String -> Bool-imgExtension ".jpg"  = True-imgExtension ".jpeg" = True-imgExtension ".png"  = True-imgExtension ".gif"  = True-imgExtension ".hdr"  = True-imgExtension ".pic"  = True-imgExtension ".bmp"  = True-imgExtension ".TGA"  = True-imgExtension ".tga"  = True-imgExtension ".tif"  = True-imgExtension ".tiff" = True-#ifdef WEBP-imgExtension ".webp" = True-#endif-imgExtension _       = False--insertHash :: FilePath -> IO (M.Map Word64 (NonEmpty FilePath) -> M.Map Word64 (NonEmpty FilePath))-insertHash fp = do-    hash <- fileHash fp-    case hash of-        Right x ->-            pure $ \hashes ->-                let go (Just others) = Just (fp <| others)-                    go Nothing       = Just (fp :| [])-                    in M.alter go x hashes-        Left err -> putStrLn ("WARNING: skipping " ++ fp ++ "\n" ++ err) $> id--stepMap :: TVar (M.Map Word64 (NonEmpty FilePath)) -> FilePath -> IO ()-stepMap var fp = do-    mod' <- insertHash fp-    atomically $ modifyTVar' var mod'--pathMaps :: [FilePath] -> IO (M.Map Word64 (NonEmpty FilePath))-pathMaps fps = do-    total <- newTVarIO mempty-    parTraverseAll (stepMap total) fileFilter (\_ -> pure True) fps-    readTVarIO total--    where fileFilter = pure . imgExtension . takeExtension
+ app/Parallel.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE ScopedTypeVariables #-}++module Parallel ( pathMaps ) where++import           Control.Concurrent.STM      (atomically)+import           Control.Concurrent.STM.TVar (TVar, modifyTVar', newTVarIO, readTVarIO)+import           Data.Functor                (($>))+import           Data.List.NonEmpty          (NonEmpty (..), (<|))+import qualified Data.Map                    as M+import           Data.Word                   (Word64)+import           PerceptualHash              (fileHash)+import           System.Directory.Parallel   (parTraverseAll)+import           System.FilePath             (takeExtension)++imgExtension :: String -> Bool+imgExtension ".jpg"  = True+imgExtension ".jpeg" = True+imgExtension ".png"  = True+imgExtension ".gif"  = True+imgExtension ".hdr"  = True+imgExtension ".pic"  = True+imgExtension ".bmp"  = True+imgExtension ".TGA"  = True+imgExtension ".tga"  = True+imgExtension ".tif"  = True+imgExtension ".tiff" = True+imgExtension ".webp" = True+imgExtension ".avif" = True+imgExtension _       = False++insertHash :: FilePath -> IO (M.Map Word64 (NonEmpty FilePath) -> M.Map Word64 (NonEmpty FilePath))+insertHash fp = do+    hash <- fileHash fp+    case hash of+        Right x ->+            pure $ \hashes ->+                let go (Just others) = Just (fp <| others)+                    go Nothing       = Just (fp :| [])+                    in M.alter go x hashes+        Left err -> putStrLn ("WARNING: skipping " ++ fp ++ "\n" ++ err) $> id++stepMap :: TVar (M.Map Word64 (NonEmpty FilePath)) -> FilePath -> IO ()+stepMap var fp = do+    mod' <- insertHash fp+    atomically $ modifyTVar' var mod'++pathMaps :: [FilePath] -> IO (M.Map Word64 (NonEmpty FilePath))+pathMaps fps = do+    total <- newTVarIO mempty+    parTraverseAll (stepMap total) fileFilter (\_ -> pure True) fps+    readTVarIO total++    where fileFilter = pure . imgExtension . takeExtension
bench/Bench.cpphs view
@@ -9,7 +9,7 @@ import           System.FilePath (takeFileName)  mkBGroup :: NFData a => String -> (FilePath -> IO a) -> Benchmark-mkBGroup str f = bgroup str (toBench <$> [catPath, frogJpeg, frogPng, lizTaylorWebp, lizTaylorPng])+mkBGroup str f = bgroup str (toBench <$> [catPath, frogJpeg, frogPng, lizTaylorWebp, lizTaylorPng, fashionPng, fashionAvif])      where toBench fp = bench (takeFileName fp) $ nfIO (f fp)           catPath = "demo-data/cat.png"@@ -17,6 +17,8 @@           frogPng = "demo-data/frog.png"           lizTaylorWebp = "demo-data/liz-taylor.webp"           lizTaylorPng = "demo-data/liz-taylor.png"+          fashionPng = "demo-data/fashion.png"+          fashionAvif = "demo-data/fashion.avif"  main :: IO () main =
+ demo-data/fashion.avif view

binary file changed (absent → 83419 bytes)

+ demo-data/fashion.png view

binary file changed (absent → 1559794 bytes)

perceptual-hash.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.0 name:               perceptual-hash-version:            0.1.4.5+version:            0.1.4.6 license:            BSD3 license-file:       LICENSE copyright:          Copyright: (c) 2019-2021 Vanessa McHale@@ -18,6 +18,8 @@     demo-data/meme-watermark.jpg     demo-data/liz-taylor.webp     demo-data/liz-taylor.png+    demo-data/fashion.avif+    demo-data/fashion.png  extra-source-files: include/hs_phash.h extra-doc-files:@@ -44,7 +46,6 @@  library     exposed-modules:    PerceptualHash-    build-tool-depends: cpphs:cpphs     hs-source-dirs:     src     other-modules:      Median     default-language:   Haskell2010@@ -57,19 +58,15 @@         vector,         primitive,         bytestring,-        JuicyPixels--    if !flag(disable-webp)-        build-depends: webp--    if !flag(disable-webp)-        cpp-options: -DWEBP+        JuicyPixels,+        avif,+        webp >=0.1.0.3      if flag(llvm)         ghc-options: -fllvm      if flag(with-phash)-        pkgconfig-depends: pHash+        extra-libraries: pHash      if flag(with-phash)         exposed-modules: ForeignHash@@ -115,7 +112,6 @@  executable phash     main-is:            Main.hs-    build-tool-depends: cpphs:cpphs     hs-source-dirs:     app     other-modules:         Paths_perceptual_hash@@ -152,7 +148,6 @@ test-suite perceptual-hash-test     type:               exitcode-stdio-1.0     main-is:            Spec.hs-    build-tool-depends: cpphs:cpphs     hs-source-dirs:     test     default-language:   Haskell2010     ghc-options:        -Wall -threaded -rtsopts "-with-rtsopts=-N -K1K"
− src/PerceptualHash.cpphs
@@ -1,110 +0,0 @@-{-# OPTIONS_GHC -fspecialize-aggressively #-}--{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE TypeFamilies     #-}--module PerceptualHash ( imgHash-                      , fileHash-                      , hammingDistance-                      ) where--import qualified Codec.Picture                 as JuicyPixels-#ifdef WEBP-import           Codec.Picture.WebP            (decodeRgb8)-#endif-import           Control.Monad.ST              (runST)-import           Data.Bits                     (Bits, popCount, shiftL, xor, (.|.))-import qualified Data.ByteString               as BS-import           Data.List                     (isSuffixOf)-import qualified Data.Vector.Generic           as V-import qualified Data.Vector.Storable          as VS-import           Data.Word                     (Word64, Word8)-import           Graphics.Image                (Array, Bilinear (..), Border (Edge, Reflect), Image,-                                                Pixel (PixelX, PixelY), RGB, RSU (..), VS, X, Y,-                                                convert, convolve, crop, makeImage, readImage,-                                                resize, transpose, (|*|))-import           Graphics.Image.Interface      (fromVector, toVector)-import qualified Graphics.Image.Interface      as Hip-import           Graphics.Image.Interface.Repa (fromRepaArrayS, toRepaArray)-import           Median                        (median)---- | See--- [wiki](https://en.wikipedia.org/wiki/Hamming_distance#Algorithm_example).------ @since 0.1.4.0-{-# SPECIALIZE hammingDistance :: Word64 -> Word64 -> Int #-}-hammingDistance :: Bits a => a -> a -> Int-hammingDistance x y = popCount (x `xor` y)--dct32 :: (Floating e, Array arr Y e) => Image arr Y e-dct32 = makeImage (32,32) gen-    where gen (i,j) = PixelY $ sqrt(2/n) * cos((fromIntegral ((2*i) * (j-1)) * pi)/(2*n))-          n = 32--idMat :: (Fractional e, Array arr X e) => Image arr X e-idMat = makeImage (7,7) (\_ -> PixelX (1/49))--{-# INLINE meanFilter #-}-meanFilter :: (Fractional e, Array arr X e, Array arr cs e) => Image arr cs e -> Image arr cs e-meanFilter = {-# SCC "meanFilter" #-} convolve Reflect idMat--{-# INLINE size32 #-}-size32 :: Array arr cs e => Image arr cs e -> Image arr cs e-size32 = resize Bilinear Edge (32,32)--crop8 :: Array arr cs e => Image arr cs e -> Image arr cs e-crop8 = crop (0,0) (8,8)--medianImmut :: (Ord e, Fractional e, V.Vector v e) => v e -> e-medianImmut v = runST $-    median =<< V.thaw v--dct :: (Floating e, Array arr Y e) => Image arr Y e -> Image arr Y e-dct img = dct32 |*| img |*| transpose dct32--{-# INLINE imgHash #-}--- | DCT based hash. See--- [Zauner](https://www.phash.org/docs/pubs/thesis_zauner.pdf).------ It is suggested that you use this with the Repa backend.-imgHash :: (Ord e, Floating e, Array arr Y e, Array arr X e, V.Vector (Hip.Vector arr) Bool, V.Vector (Hip.Vector arr) e) => Image arr Y e -> Word64-imgHash = asWord64 . aboveMed . V.map (\(PixelY x) -> x) . toVector . crop8 . dct . size32 . meanFilter--asWord64 :: V.Vector v Bool => v Bool -> Word64-asWord64 = V.foldl' (\acc x -> (acc `shiftL` 1) .|. boolToWord64 x) 0-    where boolToWord64 :: Bool -> Word64-          boolToWord64 False = 0-          boolToWord64 True  = 1--aboveMed :: (Fractional e, V.Vector v e, V.Vector v Bool, Ord e) => v e -> v Bool-aboveMed v =-    let med = medianImmut v-    in V.map (<med) v--#ifdef WEBP-{-# INLINE fileWebp #-}-fileWebp :: FilePath -> IO (Image VS RGB Word8)-fileWebp fp = do-    contents <- BS.readFile fp-    let (JuicyPixels.Image m n pixels) = decodeRgb8 contents-    pure $ fromVector (m, n) $ VS.unsafeCast pixels--{-# INLINE readWebp #-}-readWebp :: FilePath -> IO (Image VS Y Double)-readWebp = fmap convert . fileWebp--fileHashWebp :: FilePath -> IO Word64-fileHashWebp = fmap (imgHash . convRepa) . readWebp-    -- faster-    where convRepa = fromRepaArrayS . toRepaArray--fileHash :: FilePath -> IO (Either String Word64)-fileHash fp | ".webp" `isSuffixOf` fp = pure <$> fileHashWebp fp-            | otherwise = fileHashHip fp-#else-fileHash :: FilePath -> IO (Either String Word64)-fileHash = fileHashHip-#endif--fileHashHip :: FilePath -> IO (Either String Word64)-fileHashHip = fmap (fmap (imgHash :: Image RSU Y Double -> Word64)) . readImage
+ src/PerceptualHash.hs view
@@ -0,0 +1,118 @@+{-# OPTIONS_GHC -fspecialize-aggressively #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeFamilies     #-}++module PerceptualHash ( imgHash+                      , fileHash+                      , hammingDistance+                      ) where++import           Codec.Avif                    (decode)+import qualified Codec.Picture                 as JuicyPixels+import           Codec.Picture.WebP            (decodeRgb8)+import           Control.Monad.ST              (runST)+import           Data.Bits                     (Bits, popCount, shiftL, xor, (.|.))+import qualified Data.ByteString               as BS+import           Data.List                     (isSuffixOf)+import qualified Data.Vector.Generic           as V+import qualified Data.Vector.Storable          as VS+import           Data.Word                     (Word64, Word8)+import           Graphics.Image                (Array, Bilinear (..), Border (Edge, Reflect), Image,+                                                Pixel (PixelX, PixelY), RGB, RGBA, RSU (..), VS, X,+                                                Y, convert, convolve, crop, makeImage, readImage,+                                                resize, transpose, (|*|))+import           Graphics.Image.Interface      (fromVector, toVector)+import qualified Graphics.Image.Interface      as Hip+import           Graphics.Image.Interface.Repa (fromRepaArrayS, toRepaArray)+import           Median                        (median)++-- | See+-- [wiki](https://en.wikipedia.org/wiki/Hamming_distance#Algorithm_example).+--+-- @since 0.1.4.0+{-# SPECIALIZE hammingDistance :: Word64 -> Word64 -> Int #-}+hammingDistance :: Bits a => a -> a -> Int+hammingDistance x y = popCount (x `xor` y)++dct32 :: (Floating e, Array arr Y e) => Image arr Y e+dct32 = makeImage (32,32) gen+    where gen (i,j) = PixelY $ sqrt(2/n) * cos((fromIntegral (i * (j-1)) * pi)/n)+          n = 32++idMat :: (Fractional e, Array arr X e) => Image arr X e+idMat = makeImage (7,7) (\_ -> PixelX (1/49))++{-# INLINE meanFilter #-}+meanFilter :: (Fractional e, Array arr X e, Array arr cs e) => Image arr cs e -> Image arr cs e+meanFilter = {-# SCC "meanFilter" #-} convolve Reflect idMat++{-# INLINE size32 #-}+size32 :: Array arr cs e => Image arr cs e -> Image arr cs e+size32 = resize Bilinear Edge (32,32)++crop8 :: Array arr cs e => Image arr cs e -> Image arr cs e+crop8 = crop (0,0) (8,8)++medianImmut :: (Ord e, Fractional e, V.Vector v e) => v e -> e+medianImmut v = runST $+    median =<< V.thaw v++dct :: (Floating e, Array arr Y e) => Image arr Y e -> Image arr Y e+dct img = dct32 |*| img |*| transpose dct32++{-# INLINE imgHash #-}+-- | DCT based hash. See+-- [Zauner](https://www.phash.org/docs/pubs/thesis_zauner.pdf).+--+-- It is suggested that you use this with the Repa backend.+imgHash :: (Ord e, Floating e, Array arr Y e, Array arr X e, V.Vector (Hip.Vector arr) Bool, V.Vector (Hip.Vector arr) e) => Image arr Y e -> Word64+imgHash = asWord64 . aboveMed . V.map (\(PixelY x) -> x) . toVector . crop8 . dct . size32 . meanFilter++asWord64 :: V.Vector v Bool => v Bool -> Word64+asWord64 = V.foldl' (\acc x -> (acc `shiftL` 1) .|. boolToWord64 x) 0+    where boolToWord64 :: Bool -> Word64+          boolToWord64 False = 0+          boolToWord64 True  = 1++aboveMed :: (Fractional e, V.Vector v e, V.Vector v Bool, Ord e) => v e -> v Bool+aboveMed v =+    let med = medianImmut v+    in V.map (<med) v++{-# INLINE fileWebp #-}+fileWebp :: FilePath -> IO (Image VS RGB Word8)+fileWebp fp = do+    contents <- BS.readFile fp+    let (JuicyPixels.Image m n pixels) = decodeRgb8 contents+    pure $ fromVector (n, m) $ VS.unsafeCast pixels++{-# INLINE readWebp #-}+readWebp :: FilePath -> IO (Image VS Y Double)+readWebp = fmap convert . fileWebp++fileHashWebp :: FilePath -> IO Word64+fileHashWebp = fmap (imgHash . convRepa) . readWebp+    -- faster+    where convRepa = fromRepaArrayS . toRepaArray++fileHash :: FilePath -> IO (Either String Word64)+fileHash fp | ".webp" `isSuffixOf` fp = pure <$> fileHashWebp fp+            | ".avif" `isSuffixOf` fp = pure <$> fileHashAvif fp+            | otherwise = fileHashHip fp++{-# INLINE readAvif #-}+readAvif :: FilePath -> IO (Image VS Y Double)+readAvif = fmap convert . fileAvif++fileHashAvif :: FilePath -> IO Word64+fileHashAvif = fmap (imgHash . convRepa) . readAvif+    where convRepa = fromRepaArrayS . toRepaArray++{-# INLINE fileAvif #-}+fileAvif :: FilePath -> IO (Image VS RGBA Word8)+fileAvif fp = do+    (JuicyPixels.Image m n pixels) <- decode <$> BS.readFile fp+    pure $ fromVector (n, m) $ VS.unsafeCast pixels++fileHashHip :: FilePath -> IO (Either String Word64)+fileHashHip = fmap (fmap (imgHash :: Image RSU Y Double -> Word64)) . readImage
− test/Spec.cpphs
@@ -1,28 +0,0 @@-import           PerceptualHash (fileHash)-import           Test.Hspec--main :: IO ()-main = hspec $-    describe "fileHash" $ do--        parallel $ it "should match when same" $ do-            actual <- fileHash "demo-data/frog.jpeg"-            expected <- fileHash "demo-data/frog.png"-            actual `shouldBe` expected--        parallel $ it "should match when same" $ do-            actual <- fileHash "demo-data/meme-watermark.jpg"-            expected <- fileHash "demo-data/meme.png"-            actual `shouldBe` expected--        parallel $ it "should not match when different" $ do-            actual <- fileHash "demo-data/cat.png"-            expected <- fileHash "demo-data/frog.png"-            actual `shouldSatisfy` (/= expected)--#ifdef WEBP-        parallel $ it "should match when same" $ do-            actual <- fileHash "demo-data/liz-taylor.webp"-            expected <- fileHash "demo-data/liz-taylor.png"-            actual `shouldBe` expected-#endif
+ test/Spec.hs view
@@ -0,0 +1,31 @@+import           PerceptualHash (fileHash)+import           Test.Hspec++main :: IO ()+main = hspec $+    describe "fileHash" $ do++        parallel $ it "should match when same" $ do+            actual <- fileHash "demo-data/frog.jpeg"+            expected <- fileHash "demo-data/frog.png"+            actual `shouldBe` expected++        parallel $ it "should match when same" $ do+            actual <- fileHash "demo-data/meme-watermark.jpg"+            expected <- fileHash "demo-data/meme.png"+            actual `shouldBe` expected++        parallel $ it "should not match when different" $ do+            actual <- fileHash "demo-data/cat.png"+            expected <- fileHash "demo-data/frog.png"+            actual `shouldSatisfy` (/= expected)++        parallel $ it "should match when same" $ do+            actual <- fileHash "demo-data/fashion.avif"+            expected <- fileHash "demo-data/fashion.png"+            actual `shouldBe` expected++        parallel $ it "should match when same" $ do+            actual <- fileHash "demo-data/liz-taylor.webp"+            expected <- fileHash "demo-data/liz-taylor.png"+            actual `shouldBe` expected