hakyll-images 1.1.1 → 1.2.0
raw patch · 11 files changed
+74/−77 lines, 11 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Hakyll.Images: type JpgQuality = Int
- Hakyll.Images.CompressJpg: type JpgQuality = Int
+ Hakyll.Images: data JpgQuality
+ Hakyll.Images.CompressJpg: data JpgQuality
+ Hakyll.Images.CompressJpg: instance GHC.Classes.Eq Hakyll.Images.CompressJpg.JpgQuality
+ Hakyll.Images.CompressJpg: instance GHC.Classes.Ord Hakyll.Images.CompressJpg.JpgQuality
+ Hakyll.Images.CompressJpg: instance GHC.Enum.Enum Hakyll.Images.CompressJpg.JpgQuality
+ Hakyll.Images.CompressJpg: instance GHC.Num.Num Hakyll.Images.CompressJpg.JpgQuality
+ Hakyll.Images.CompressJpg: instance GHC.Real.Integral Hakyll.Images.CompressJpg.JpgQuality
+ Hakyll.Images.CompressJpg: instance GHC.Real.Real Hakyll.Images.CompressJpg.JpgQuality
- Hakyll.Images: compressJpg :: JpgQuality -> Image -> Image
+ Hakyll.Images: compressJpg :: Integral a => a -> Image -> Image
- Hakyll.Images: compressJpgCompiler :: JpgQuality -> Item Image -> Compiler (Item Image)
+ Hakyll.Images: compressJpgCompiler :: Integral a => a -> Item Image -> Compiler (Item Image)
- Hakyll.Images.CompressJpg: compressJpg :: JpgQuality -> Image -> Image
+ Hakyll.Images.CompressJpg: compressJpg :: Integral a => a -> Image -> Image
- Hakyll.Images.CompressJpg: compressJpgCompiler :: JpgQuality -> Item Image -> Compiler (Item Image)
+ Hakyll.Images.CompressJpg: compressJpgCompiler :: Integral a => a -> Item Image -> Compiler (Item Image)
Files
- CHANGELOG.md +4/−0
- hakyll-images.cabal +1/−1
- library/Hakyll/Images.hs +1/−1
- library/Hakyll/Images/Common.hs +4/−5
- library/Hakyll/Images/CompressJpg.hs +29/−14
- library/Hakyll/Images/Metadata.hs +1/−1
- library/Hakyll/Images/Resize.hs +5/−5
- tests/Hakyll/Images/Common/Tests.hs +3/−6
- tests/Hakyll/Images/CompressJpg/Tests.hs +22/−37
- tests/Hakyll/Images/Metadata/Tests.hs +2/−5
- tests/Hakyll/Images/Resize/Tests.hs +2/−2
CHANGELOG.md view
@@ -1,5 +1,9 @@ # Change log +## Release 1.2.0 + +* Made `JpgQuality` an opaque constructor, so that we can normalize the input to the range [0, 100]. + ## Release 1.1.1 * Updated the test suite to support `Hakyll.Core.Runtime.RunMode` (added in hakyll 4.15) (#10).
hakyll-images.cabal view
@@ -1,7 +1,7 @@ cabal-version: 2.2 name: hakyll-images -version: 1.1.1 +version: 1.2.0 synopsis: Hakyll utilities to work with images description: hakyll-images is an add-on to the hakyll package. It adds utilities to work with images, including JPEG compression. category: Web
library/Hakyll/Images.hs view
@@ -1,7 +1,7 @@ -- | -- Module : Hakyll.Images -- Description : Hakyll utilities for image files --- Copyright : (c) Laurent P René de Cotret, 2019 +-- Copyright : (c) Laurent P René de Cotret, 2019 - present -- License : BSD3 -- Maintainer : laurent.decotret@outlook.com -- Stability : unstable
library/Hakyll/Images/Common.hs view
@@ -1,12 +1,10 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE TypeSynonymInstances #-} - -- | -- Module : Hakyll.Images.Common -- Description : Types and utilities for Hakyll.Images --- Copyright : (c) Laurent P René de Cotret, 2019 +-- Copyright : (c) Laurent P René de Cotret, 2019 - present -- License : BSD3 -- Maintainer : laurent.decotret@outlook.com -- Stability : unstable @@ -25,6 +23,7 @@ import Data.ByteString (ByteString) import Data.ByteString.Lazy (toStrict) import Data.Char (toLower) +import Data.Either (fromRight) import Data.Typeable (Typeable) import GHC.Generics (Generic) import Hakyll.Core.Compiler (Compiler, getResourceLBS, getUnderlyingExtension) @@ -74,7 +73,7 @@ loadImage = do content <- fmap toStrict <$> getResourceLBS fmt <- fromExt <$> getUnderlyingExtension - return $ (Image fmt) <$> content + return $ Image fmt <$> content -- | Translation between file extensions and image formats. -- It is important to keep track of image formats because Hakyll @@ -99,4 +98,4 @@ encode Png im = Image Png $ (toStrict . imageToPng) im encode Bitmap im = Image Bitmap $ (toStrict . imageToBitmap) im encode Tiff im = Image Tiff $ (toStrict . imageToTiff) im -encode Gif im = Image Gif $ (toStrict . either (const $ error "Could not parse gif") id . imageToGif) im +encode Gif im = Image Gif $ (toStrict . fromRight (error "Could not parse gif") . imageToGif) im
library/Hakyll/Images/CompressJpg.hs view
@@ -1,7 +1,8 @@+{-# LANGUAGE GeneralisedNewtypeDeriving #-} -- | -- Module : Hakyll.Images.CompressJpg -- Description : Hakyll compiler to compress Jpeg images --- Copyright : (c) Laurent P René de Cotret, 2019 +-- Copyright : (c) Laurent P René de Cotret, 2019 - present -- License : BSD3 -- Maintainer : laurent.decotret@outlook.com -- Stability : unstable @@ -47,27 +48,41 @@ format, image, ) +import Numeric.Natural (Natural) + -- | Jpeg encoding quality, from 0 (lower quality) to 100 (best quality). -type JpgQuality = Int +-- @since 1.2.0 +newtype JpgQuality = JpgQuality Natural + deriving (Num, Eq, Enum, Ord, Real, Integral) + +-- | @JpgQuality@ smart constructor. Ensures that @JpgQuality@ is always +-- in the interval [0, 100]. Numbers outside this range will result in either +-- a quality of 0 or 100. +-- +-- @since 1.2.0 +mkJpgQuality :: Integral a => a -> JpgQuality +mkJpgQuality q | q < 0 = JpgQuality 0 + | q > 100 = JpgQuality 100 + | otherwise = JpgQuality (fromIntegral q) + + -- | Compress a JPG bytestring to a certain quality setting. -- The quality should be between 0 (lowest quality) and 100 (best quality). --- An error is raised if the image cannot be decoded, or if the --- encoding quality is out-of-bounds -compressJpg :: JpgQuality -> Image -> Image -compressJpg quality src = - if (format src) /= Jpeg - then error $ "Image is not a JPEG." +-- An error is raised if the image cannot be decoded. +compressJpg :: Integral a => a -> Image -> Image +compressJpg quality' src = + if format src /= Jpeg + then error "Image is not a JPEG." else case decodeJpeg $ image src of - Left _ -> error $ "Loading the image failed." - Right dynImage -> - if (quality < 0 || quality > 100) - then error $ "JPEG encoding quality should be between 0 and 100." - else Image Jpeg $ (toStrict $ imageToJpg quality dynImage) + Left _ -> error "Loading the image failed." + Right dynImage -> Image Jpeg $ toStrict (imageToJpg (fromIntegral quality) dynImage) + where quality = mkJpgQuality quality' -- | Compiler that compresses a JPG image to a certain quality setting. -- The quality should be between 0 (lowest quality) and 100 (best quality). +-- Values outside of this range will be normalized to the interval [0, 100]. -- An error is raised if the image cannot be decoded. -- -- @ @@ -76,5 +91,5 @@ -- compile $ loadImage -- >>= compressJpgCompiler 50 -- @ -compressJpgCompiler :: JpgQuality -> Item Image -> Compiler (Item Image) +compressJpgCompiler :: Integral a => a -> Item Image -> Compiler (Item Image) compressJpgCompiler quality = return . fmap (compressJpg quality)
library/Hakyll/Images/Metadata.hs view
@@ -1,7 +1,7 @@ -- | -- Module : Hakyll.Images.Metadata -- Description : Handling image metadata --- Copyright : (c) Laurent P René de Cotret, 2019 +-- Copyright : (c) Laurent P René de Cotret, 2019 - present -- License : BSD3 -- Maintainer : laurent.decotret@outlook.com -- Stability : unstable
library/Hakyll/Images/Resize.hs view
@@ -1,7 +1,7 @@ -- | -- Module : Hakyll.Images.Resize -- Description : Hakyll compiler to resize images --- Copyright : (c) Laurent P René de Cotret, 2019 +-- Copyright : (c) Laurent P René de Cotret, 2019 - present -- License : BSD3 -- Maintainer : laurent.decotret@outlook.com -- Stability : unstable @@ -72,7 +72,7 @@ -- In the process, an image is converted to RGBA8. Therefore, some information -- loss may occur. resize :: Width -> Height -> DynamicImage -> DynamicImage -resize w h = ImageRGBA8 . (scaleBilinear w h) . convertRGBA8 +resize w h = ImageRGBA8 . scaleBilinear w h . convertRGBA8 -- | Scale an image to a size that will fit in the specified width and height, -- while preserving aspect ratio. Images might be scaled up as well. @@ -139,7 +139,7 @@ resizeImageCompiler :: Width -> Height -> Item Image -> Compiler (Item Image) resizeImageCompiler w h item = let fmt = (format . itemBody) item - in return $ (encode fmt . resize w h . decodeImage' . image) <$> item + in return $ encode fmt . resize w h . decodeImage' . image <$> item -- | Compiler that rescales images to fit within dimensions. Aspect ratio -- will be preserved. Images might be scaled up as well. @@ -156,7 +156,7 @@ scaleImageCompiler :: Width -> Height -> Item Image -> Compiler (Item Image) scaleImageCompiler w h item = let fmt = (format . itemBody) item - in return $ (encode fmt . scale w h . decodeImage' . image) <$> item + in return $ encode fmt . scale w h . decodeImage' . image <$> item -- | Compiler that ensures images will fit within dimensions. Images might -- be scaled down, but never up. Aspect ratio will be preserved. @@ -173,4 +173,4 @@ ensureFitCompiler :: Width -> Height -> Item Image -> Compiler (Item Image) ensureFitCompiler w h item = let fmt = (format . itemBody) item - in return $ (encode fmt . ensureFit w h . decodeImage' . image) <$> item + in return $ encode fmt . ensureFit w h . decodeImage' . image <$> item
tests/Hakyll/Images/Common/Tests.hs view
@@ -64,18 +64,15 @@ route idRoute compile $ loadImage - >>= compressJpgCompiler 50 + >>= compressJpgCompiler (50::Int) _ <- assertBool "Image was not written" - <$> (doesFileExist $ destinationDirectory testConfiguration </> "piccolo.jpg") + <$> doesFileExist ( destinationDirectory testConfiguration </> "piccolo.jpg") cleanTestEnv -------------------------------------------------------------------------------- tests :: TestTree tests = - testGroup "Hakyll.Images.Common.Tests" $ - concat - [ fromAssertions "run" [case1] - ] + testGroup "Hakyll.Images.Common.Tests" $ fromAssertions "run" [case1]
tests/Hakyll/Images/CompressJpg/Tests.hs view
@@ -8,16 +8,15 @@ -------------------------------------------------------------------------------- -import Control.Exception (ErrorCall, catch, evaluate) import qualified Data.ByteString as B -import Hakyll.Images.Common -import Hakyll.Images.CompressJpg +import Hakyll.Images.Common ( Image(Image, image), ImageFormat(Jpeg) ) +import Hakyll.Images.CompressJpg ( compressJpg ) import Test.Tasty (TestTree, testGroup) -import Test.Tasty.HUnit (Assertion, assertBool, assertFailure, testCase) +import Test.Tasty.HUnit (Assertion, assertBool, testCase) import Text.Printf (printf) testJpg :: IO Image -testJpg = Image Jpeg <$> (B.readFile "tests/data/piccolo.jpg") +testJpg = Image Jpeg <$> B.readFile "tests/data/piccolo.jpg" fromAssertions :: -- | Name @@ -35,51 +34,37 @@ testCompressionFromImage = do im <- testJpg let initialSize = (B.length . image) im - finalSize = (B.length . image . compressJpg 25) im + finalSize = (B.length . image . compressJpg (25::Int)) im assertBool "Image was not compressed" (initialSize > finalSize) --- Test that specifying a JPG encoding below 0 will fail +-- Test that specifying a JPG encoding below 0 will not fail testJpgEncodingOutOfLowerBound :: Assertion testJpgEncodingOutOfLowerBound = do im <- testJpg - -- Catching exceptions is an idea from here: - -- https://stackoverflow.com/questions/46330592/is-it-possible-to-assert-an-error-case-in-hunit - -- Since compressJpg is a "pure" function, we need to evaluate it in an IO context - -- to catch errors. - errored <- catch (evaluate (compressJpg (-10) im) >> pure False) handler - if errored - then pure () - else assertFailure "did not catch expected error" - where - handler :: ErrorCall -> IO Bool - handler _ = pure True + + let compressedSize = (B.length . image . compressJpg (-10 :: Int)) im + expectedSize = (B.length . image . compressJpg (0 :: Int)) im + assertBool "Out-of-boumds JpgQuality was not handled properly" (expectedSize == compressedSize) + -- Test that specifying a JPG encoding above 100 will fail testJpgEncodingOutOfUpperBound :: Assertion testJpgEncodingOutOfUpperBound = do im <- testJpg - -- Catching exceptions is an idea from here: - -- https://stackoverflow.com/questions/46330592/is-it-possible-to-assert-an-error-case-in-hunit - -- Since compressJpg is a "pure" function, we need to evaluate it in an IO context - -- to catch errors. - errored <- catch (evaluate (compressJpg 111 im) >> pure False) handler - if errored - then pure () - else assertFailure "did not catch expected error" - where - handler :: ErrorCall -> IO Bool - handler _ = pure True + + let compressedSize = (B.length . image . compressJpg (150 :: Int)) im + expectedSize = (B.length . image . compressJpg (100 :: Int)) im + assertBool "Out-of-boumds JpgQuality was not handled properly" (expectedSize == compressedSize) + -------------------------------------------------------------------------------- tests :: TestTree tests = testGroup "Hakyll.Images.CompressJpg.Tests" $ - concat - [ fromAssertions - "compressJpg" - [ testCompressionFromImage, - testJpgEncodingOutOfLowerBound, - testJpgEncodingOutOfUpperBound - ] - ] + fromAssertions + "compressJpg" + [ testCompressionFromImage, + testJpgEncodingOutOfLowerBound, + testJpgEncodingOutOfUpperBound + ]
tests/Hakyll/Images/Metadata/Tests.hs view
@@ -29,7 +29,7 @@ zipWith testCase [printf "[%2d] %s" n name | n <- [1 :: Int ..]] testJpg :: IO Image -testJpg = Image Jpeg <$> (B.readFile "tests/data/piccolo.jpg") +testJpg = Image Jpeg <$> B.readFile "tests/data/piccolo.jpg" testMetadata :: Assertion testMetadata = do @@ -45,7 +45,4 @@ tests :: TestTree tests = - testGroup "Hakyll.Images.Metadata.Tests" $ - concat - [ fromAssertions "metadata" [testMetadata] - ] + testGroup "Hakyll.Images.Metadata.Tests" $ fromAssertions "metadata" [testMetadata]
tests/Hakyll/Images/Resize/Tests.hs view
@@ -101,9 +101,9 @@ testScalePreservesAspectRatio = do image <- testJpg - let initialAspectRatio = (imageWidth $ convertRGBA8 image) % (imageHeight $ convertRGBA8 image) + let initialAspectRatio = imageWidth (convertRGBA8 image) % imageHeight (convertRGBA8 image) scaledImage = scale 600 400 image - finalAspectRatio = (imageWidth $ convertRGBA8 scaledImage) % (imageHeight $ convertRGBA8 scaledImage) + finalAspectRatio = imageWidth (convertRGBA8 scaledImage) % imageHeight (convertRGBA8 scaledImage) assertApproxEqual "Aspect ratio was not preserved" 0.02 initialAspectRatio finalAspectRatio