-- | -- Module : Graphics.Identicon -- Copyright : © 2016–2017 Mark Karpov -- License : BSD 3 clause -- -- Maintainer : Mark Karpov <markkarpov92@gmail.com> -- Stability : experimental -- Portability : portable -- -- Core types and definitions for flexible generation of identicons. Please -- see the "Graphics.Identicon.Primitive" module for a collection of -- building blocks to code layers of your identicon. -- -- A basic complete example looks like this: -- -- > import Codec.Picture -- > import Data.ByteString (ByteString) -- > import Data.Proxy -- > import Data.Word (Word8) -- > import Graphics.Identicon -- > import Graphics.Identicon.Primitive -- > -- > myImageType :: Proxy (Identicon 4 :+ Consumer 4) -- > myImageType = Proxy -- > -- > myImpl = Identicon :+ a -- > where -- > a :: Word8 -> Word8 -> Word8 -> Word8 -> Layer -- > a r g b n = rsym $ onGrid 6 6 n $ -- > circle $ gradientLR (edge . mid) black (PixelRGB8 r g b) -- > -- > myGenerator :: Int -> Int -> ByteString -> Maybe (Image PixelRGB8) -- > myGenerator = renderIdenticon myImageType myImpl -- -- @myGenerator@ takes desired width, height, and hash that should have at -- least 4 bytes in it and returns an identicon corresponding to that hash -- or 'Nothing' if the hash has less than 4 bytes in it or width\/height -- don't make sense. The identicon has randomly placed circle with gradient -- filling changing (horizontally) from black to some color and back to -- black. The circle is mirrored 4 times, and every repetition is rotated by -- 90°. This identicon consumes 4 bytes and has one layer. {-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} module Graphics.Identicon ( -- * Basic types Identicon (..) , Consumer , (:+) (..) , Layer (..) , BytesAvailable , BytesConsumed , Implementation , ToLayer -- * Identicon rendering , Renderable (..) , ApplyBytes (..) , renderIdenticon ) where import Codec.Picture import Data.ByteString (ByteString) import Data.Maybe (fromJust) import Data.Proxy import Data.Word (Word8) import GHC.TypeLits import qualified Data.ByteString as B import qualified Data.Semigroup as S #if !MIN_VERSION_base(4,8,0) import Data.Monoid #endif ---------------------------------------------------------------------------- -- Basic types -- | 'Identicon' is a type that represents an identicon consisting of zero -- layers. The type is parametrized over a phantom type @n@ which is a -- natural number on the type level that represents the number of bytes that -- should be provided to generate this type of identicon. Bytes typically -- come from some sort of hash that has a fixed size. data Identicon (n :: Nat) = Identicon -- | 'Consumer' is a type that represents an entity that consumes bytes that -- are available for identicon generation. It's parametrized over a phantom -- type @n@ which is a natural number on the type level that represents the -- number of bytes that this entity consumes. At this moment, a 'Consumer' -- always adds one 'Layer' to identicon when attached to it. The number of -- bytes, specified as type parameter of 'Identicon' type must be completely -- consumed by a collection of consumers attached to it. To attach a -- consumer to 'Identicon', you use the @(':+')@ type operator, see below. data Consumer (n :: Nat) -- | The @(':+')@ type operator is used to attach 'Consumer's to -- 'Identicon', thus adding layers to it and exhausting bytes that are -- available for identicon generation. An example of identicon that can be -- generated from 16 byte hash is shown below: -- -- > type Icon = Identicon 16 :+ Consumer 5 :+ Consumer 5 :+ Consumer 6 -- -- The identicon above has three layers. infixl 8 :+ data a :+ b = a :+ b -- | 'Layer' is the basic building block of an identicon. It's a function -- that takes the following arguments (in order): -- -- * Width of identicon -- * Height of identicon -- * Position on X axis -- * Position on Y axis -- -- …and returns a 'PixelRGB8' value. In this library, an identicon is -- generated as a “superposition” of several 'Layers'. newtype Layer = Layer { unLayer :: Int -> Int -> Int -> Int -> PixelRGB8 } instance S.Semigroup Layer where Layer a <> Layer b = Layer (\w h -> mixPixels (a w h) (b w h)) instance Monoid Layer where mempty = Layer $ \_ _ _ _ -> PixelRGB8 0 0 0 mappend = (S.<>) -- | The 'BytesAvailable' type function calculates how many bytes are -- available for consumption in a given identicon. type family BytesAvailable a :: Nat where BytesAvailable (Identicon n) = n BytesAvailable (x :+ y) = BytesAvailable x -- | The 'BytesConsumed' type function calculates how many bytes are -- consumed in a given identicon. type family BytesConsumed a :: Nat where BytesConsumed (Identicon n) = 0 BytesConsumed (Consumer n) = n BytesConsumed (x :+ y) = BytesConsumed x + BytesConsumed y -- | The 'Implementation' type function returns the type of the code which -- can implement the given identicon. type family Implementation a where Implementation (Identicon n) = Identicon n Implementation (a :+ Consumer n) = Implementation a :+ ToLayer n -- | The 'ToLayer' type function calculates type that a layer-producing -- function should have to consume the given number of bytes @n@. type family ToLayer (n :: Nat) :: k where ToLayer 0 = Layer ToLayer n = Word8 -> ToLayer (n - 1) ---------------------------------------------------------------------------- -- Identicon rendering -- | Identicons that can be rendered as an image implement this class. class Renderable a where render :: Proxy a -- ^ A 'Proxy' clarifying identicon type -> Implementation a -- ^ Corresponding implementation -> Int -- ^ Width in pixels -> Int -- ^ Height in pixels -> ByteString -- ^ Bytes to consume -> (ByteString, Int -> Int -> PixelRGB8) -- ^ The rest of bytes and producing function instance Renderable (Identicon n) where render _ _ _ _ bs = (bs, \_ _ -> PixelRGB8 0 0 0) instance (Renderable a, ApplyBytes (ToLayer n)) => Renderable (a :+ Consumer n) where render _ (a :+ b) weight height bs0 = let (bs1, x) = render (Proxy :: Proxy a) a weight height bs0 (bs2, y) = applyBytes b bs1 in (bs2, mixPixels x (unLayer y weight height)) -- | Combine results of two rending functions. mixPixels :: (Int -> Int -> PixelRGB8) -> (Int -> Int -> PixelRGB8) -> Int -> Int -> PixelRGB8 mixPixels a b x y = mixWith (const saturatedAddition) (a x y) (b x y) {-# INLINE mixPixels #-} -- | An implementation of saturated addition for bytes. saturatedAddition :: Word8 -> Word8 -> Word8 saturatedAddition x y = let z = x + y in if z < x then 0xff else z {-# INLINE saturatedAddition #-} -- | Consume bytes from a strict 'ByteString' and apply them to a function -- that takes 'Word8' until it produces a 'Layer'. class ApplyBytes a where applyBytes :: a -- ^ Function that produces a layer -> ByteString -- ^ Bytes to consume -> (ByteString, Layer) -- ^ The rest of 'ByteString' and produced 'Layer' instance ApplyBytes Layer where applyBytes f bs = (bs, f) instance ApplyBytes f => ApplyBytes (Word8 -> f) where applyBytes f bs = let (b,bs') = fromJust (B.uncons bs) in applyBytes (f b) bs' -- | Render an identicon. The function returns 'Nothing' if given -- 'ByteString' is too short or when width or height is lesser than 1. renderIdenticon :: forall a. ( Renderable a , KnownNat (BytesAvailable a) , BytesAvailable a ~ BytesConsumed a ) => Proxy a -- ^ Type that defines an identicon -> Implementation a -- ^ Implementation that generates layers -> Int -- ^ Width in pixels -> Int -- ^ Height in pixels -> ByteString -- ^ Collection of bytes to use, should be long enough -> Maybe (Image PixelRGB8) -- ^ Rendered identicon, or 'Nothing' if there is not enough bytes renderIdenticon proxy impl width height bs = if B.length bs < fromIntegral (natVal (Proxy :: Proxy (BytesAvailable a))) || width < 1 || height < 1 then Nothing else Just $ generateImage (snd $ render proxy impl width height bs) width height {-# NOINLINE renderIdenticon #-}