{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Blurhash where import Control.Monad (void) import Codec.Picture import System.FilePath import qualified Data.ByteString.Lazy as BS import qualified Data.Vector.Storable as V import Hedgehog import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range import Test.Tasty.HUnit import qualified Codec.Picture.Blurhash as BH imgDir :: FilePath imgDir = takeDirectory __FILE__ ".." "imgs" readAsset :: FilePath -> IO DynamicImage readAsset fp = either assertFailure pure =<< (readImage $ imgDir fp) unit_testEncodeKnownHashPurePython :: IO () unit_testEncodeKnownHashPurePython = do img <- readAsset "cool_cat.jpg" hash <- either (assertFailure . show) pure $ BH.encodeDynamic img assertEqual "Correct hash" "UBMOZfK1GG%LBBNG,;Rj2skq=eE1s9n4S5Na" hash unit_testBadXComponents :: IO () unit_testBadXComponents = do img <- readAsset "cool_cat.jpg" let res = BH.encodeDynamicWithConfig BH.encodeConfigDefault {BH.componentsX = 0} img case res of Left BH.InvalidComponents -> pure () _ -> assertFailure $ "Unexpected response" <> show res unit_testBadYComponents :: IO () unit_testBadYComponents = do img <- readAsset "cool_cat.jpg" let res = BH.encodeDynamicWithConfig BH.encodeConfigDefault {BH.componentsY = 10} img case res of Left BH.InvalidComponents -> pure () _ -> assertFailure $ "Unexpected response " <> show res unit_testBadHashLength :: IO () unit_testBadHashLength = do let res = BH.decodeRGB8 "bogus" case res of Left BH.InvalidHashLength -> pure () _ -> assertFailure $ "Unexpected response " <> either show showImgRGB8 res unit_testInvalidCharacter :: IO () unit_testInvalidCharacter = do let res = BH.decodeRGB8 "``````````````````````" case res of Left (BH.InvalidCharacterError char) -> assertEqual "Correct byte" (BS.singleton char) "`" _ -> assertFailure $ "Unexpected response " <> either show showImgRGB8 res genValidEncodeConfig :: Gen BH.EncodeConfig genValidEncodeConfig = do compX <- genValidComponent compY <- genValidComponent pure $ BH.encodeConfigDefault { BH.componentsX = compX, BH.componentsY = compY } genInvalidEncodeConfigWith :: Gen Int -> Gen Int -> Gen BH.EncodeConfig genInvalidEncodeConfigWith genX genY = do compX <- genX compY <- genY pure BH.encodeConfigDefault { BH.componentsX = compX, BH.componentsY = compY } genInvalidEncodeConfig :: Gen BH.EncodeConfig genInvalidEncodeConfig = Gen.frequency [ (4, genInvalidEncodeConfigWith genInvalidComponent genValidComponent) , (4, genInvalidEncodeConfigWith genValidComponent genInvalidComponent) , (2, genInvalidEncodeConfigWith genInvalidComponent genInvalidComponent)] genValidComponent :: Gen Int genValidComponent = Gen.integral $ Range.linear 1 9 genInvalidComponent :: Gen Int genInvalidComponent = Gen.choice [Gen.integral $ Range.linear (-10) 0, Gen.integral $ Range.linear 1 9] genValidDecodeConfig :: Gen BH.DecodeConfig genValidDecodeConfig = do punch <- Gen.float $ Range.linearFrac 0.5 3 width <- Gen.integral $ Range.linear 10 50 height <- Gen.integral $ Range.linear 10 50 pure $ BH.decodeConfigDefault { BH.punch = punch, BH.outputWidth = width, BH.outputHeight = height} genRGB8 :: MonadGen m => m PixelRGB8 genRGB8 = PixelRGB8 <$> Gen.enumBounded <*> Gen.enumBounded <*> Gen.enumBounded genValidRGB8Img :: Int -> Int -> Gen (Image PixelRGB8) genValidRGB8Img width height = do imgData <- V.fromList <$> Gen.list (Range.singleton $ width * height) Gen.enumBounded pure $ Image width height imgData hprop_validComponentsValidBlur :: Property hprop_validComponentsValidBlur = property $ do inConfig <- forAll genValidEncodeConfig width <- forAll $ Gen.integral $ Range.linear 10 100 height <- forAll $ Gen.integral $ Range.linear 10 100 img <- forAllWith showImgRGB8 $ genValidRGB8Img width height blurhash <- evalEither $ BH.encodeRGB8WithConfig inConfig img outConfig <- forAll genValidDecodeConfig void $ evalEither $ BH.decodeRGB8WithConfig outConfig blurhash hprop_encodeRGB8DoesNotThrow :: Property hprop_encodeRGB8DoesNotThrow = property $ do config <- forAll $ Gen.choice [genInvalidEncodeConfig, genInvalidEncodeConfig] imgData <- forAll $ Gen.list (Range.linear 0 400) Gen.enumBounded w <- forAll $ Gen.integral $ Range.linear 0 100 h <- forAll $ Gen.integral $ Range.linear 0 100 let img = Image w h (V.fromList imgData) case BH.encodeRGB8WithConfig config img of Left BH.InvalidComponents -> label "Invalid components" Left (BH.B83EncodingError _ _) -> label "Base83 encoding error" Right _ -> label "Encoded" hprop_encodeLinearDoesNotThrow :: Property hprop_encodeLinearDoesNotThrow = property $ do config <- forAll $ Gen.choice [genInvalidEncodeConfig, genInvalidEncodeConfig] imgData <- forAll $ Gen.list (Range.linear 0 400) (Gen.float (Range.linearFrac (-100) 100)) w <- forAll $ Gen.integral $ Range.linear 0 100 h <- forAll $ Gen.integral $ Range.linear 0 100 let img = Image w h (V.fromList imgData) case BH.encodeLinearWithConfig config img of Left BH.InvalidComponents -> label "Invalid components" Left (BH.B83EncodingError _ _) -> label "Base83 encoding error" Right _ -> label "Encoded" hprop_decodeDoesNotThrow :: Property hprop_decodeDoesNotThrow = property $ do blurhash <- forAll $ Gen.bytes $ Range.linear 0 1000 config <- forAll $ genValidDecodeConfig case BH.decodeRGB8WithConfig config $ BS.fromStrict blurhash of Left BH.InvalidHashLength -> label "Invalid hash length" Left (BH.InvalidCharacterError _) -> label "Invalid char error" Right _ -> label "Decoded" showImgRGB8 :: Image PixelRGB8 -> String showImgRGB8 img = "Width: " ++ (show $ imageWidth img) ++ ", Height: " ++ (show $ imageWidth img) ++ ", Bytes: " ++ (show $ V.length $ imageData img)