packages feed

hakyll-images 0.0.1 → 0.1.0

raw patch · 10 files changed

+292/−30 lines, 10 filesdep +HUnit-approxdep +JuicyPixels-extradep ~JuicyPixels

Dependencies added: HUnit-approx, JuicyPixels-extra

Dependency ranges changed: JuicyPixels

Files

CHANGELOG.md view
@@ -1,7 +1,12 @@ # Change log -hakyll-images uses [Semantic Versioning][].-The change log is available through the [releases on GitHub][].+Release 0.1.0 (Work-in-progress)+-------------------------------- -[Semantic Versioning]: http://semver.org/spec/v2.0.0.html-[releases on GitHub]: https://github.com/githubuser/hakyll-images/releases+* added resizeImageCompiler to resize images to a specific shape;+* added scaleImageCompiler to scale images while keeping aspect ratio.++Release 0.0.1+-------------++* Added compressJpgCompiler to compress JPEGs.
README.md view
@@ -1,43 +1,88 @@ # hakyll-images -[![Build status](https://ci.appveyor.com/api/projects/status/kf12xsgrx1l26b3y?svg=true)](https://ci.appveyor.com/project/LaurentRDC/hakyll-images)+[![Hackage version](https://img.shields.io/hackage/v/hakyll-images.svg)](http://hackage.haskell.org/package/hakyll-images) [![Build status](https://ci.appveyor.com/api/projects/status/kf12xsgrx1l26b3y?svg=true)](https://ci.appveyor.com/project/LaurentRDC/hakyll-images)  ## A Haskell package containing utilities to deal with images in the context of Hakyll  [Hakyll](https://hackage.haskell.org/package/hakyll) is a static website compiler library. As one of the benefits of static websites is their small size, this repository aims at providing utilities to work with images in the context of Hakyll. Example usage includes:  * Re-encoding Jpeg images at a lower quality to make them much smaller;-* Re-sizing images to fit within a certain shape+* Re-sizing images to fit within a certain shape;  ## Usage -`hakyll-images` is meant to be integrated within a Hakyll program. For example, to compress all Jpeg images present in your source:+`hakyll-images` is meant to be integrated within a Hakyll program. Here are some examples within a Hakyll program:  ```haskell import Hakyll-import Hakyll.Images        (compressJpgCompiler)+import Hakyll.Images        ( compressJpgCompiler+                            , resizeImageCompiler+                            , scaleImageCompiler+                            ) -(... omitted ...)+(...)  hakyll $ do -    (... omitted ...)-     -- Compress all source Jpegs to a Jpeg quality of 50 (maximum of 100)     match "images/**.jpg" $ do-            route idRoute-            compile (compressJpgCompiler 50)+        route idRoute+        compile (compressJpgCompiler 50) -    (... omitted ...)+    -- Resize all profile pictures to 64x48+    -- Aspect ratio might be mangled+    match "profiles/**.jpg" $ do+        route idRoute+        compile (resizeImageCompiler 64 48)++    -- Scale images to fit within a 600x400 box+    -- Aspect ratio will be preserved+    match "images/**.png" $ do+        route idRoute+        compile (scaleImageCompiler 600 400)++    (...) ```  Take a look at the [documentation](hackage.haskell.org/package/hakyll-images) for more usage examples. +If you would like a feature added, consider creating [an issue on Github](https://github.com/LaurentRDC/hakyll-images/issues/)+ ## Installation -`hakyll-images` is available on Hackage. Using the [`cabal-install`](https://www.haskell.org/cabal/) tool:+### From Hackage +`hakyll-images` is available on [Hackage](https://hackage.haskell.org). Using the [`cabal-install`](https://www.haskell.org/cabal/) tool:+ ```bash cabal update cabal install hakyll-images ```++### From source++Building from source can be done using [`stack`](https://docs.haskellstack.org/en/stable/README/) or [`cabal`](https://www.haskell.org/cabal/):++```bash+git clone github.com/LaurentRDC/hakyll-images.git+cd hakyll-images+stack install # Alternatively, `cabal install`+```++## Documentation++The documentation for the latest release is available on the [Hackage page](http://hackage.haskell.org/package/hakyll-images/). ++## Upcoming features++Here are the upcoming features of `hakyll-images`:++- [ ] Format conversion++## Support  Issues / Feature requests++All support requests (e.g. installation issues, unclear documentation, bugs, etc.) should of [filed on Github as an issue](https://github.com/LaurentRDC/hakyll-images/issues/)++## License++This package is made available under the BSD 3-clause license. For more details, see the [LICENSE.md](https://github.com/LaurentRDC/hakyll-images/blob/master/LICENSE.md)
hakyll-images.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: b554c0eda8c5af856ccb735a5ce03132b302a37ce1c444073df2c58b97e755ba+-- hash: 947315fcd96241760b35b112def30618422d1da5a3b0024b6d183d8d0075da1d  name:           hakyll-images-version:        0.0.1+version:        0.1.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@@ -35,13 +35,15 @@   exposed-modules:       Hakyll.Images       Hakyll.Images.CompressJpg+      Hakyll.Images.Resize   other-modules:       Paths_hakyll_images   hs-source-dirs:       library   ghc-options: -Wall -Wcompat   build-depends:-      JuicyPixels >3 && <4+      JuicyPixels >=3+    , JuicyPixels-extra >0.3     , base >=4.8 && <5     , bytestring >=0.9 && <0.11     , hakyll >4@@ -52,15 +54,19 @@   main-is: TestSuite.hs   other-modules:       Hakyll.Images.CompressJpg.Tests+      Hakyll.Images.Resize.Tests       Hakyll.Images       Hakyll.Images.CompressJpg+      Hakyll.Images.Resize       Paths_hakyll_images   hs-source-dirs:       tests       library   ghc-options: -Wall -Wcompat   build-depends:-      JuicyPixels >3 && <4+      HUnit-approx >=1.0.0.0+    , JuicyPixels >=3+    , JuicyPixels-extra >0.3     , base >=4.8 && <5     , bytestring >=0.9 && <0.11     , hakyll >4
library/Hakyll/Images.hs view
@@ -3,7 +3,7 @@ Module      : Hakyll.Images
 Description : Hakyll utilities for image files
 Copyright   : (c) Laurent P René de Cotret, 2018
-License     : MIT
+License     : BSD3
 Maintainer  : laurent.decotret@outlook.com
 Stability   : stable
 Portability : portable
@@ -13,6 +13,11 @@       JpgQuality
     , compressJpg
     , compressJpgCompiler
+    -- Image scaling
+    , Width, Height
+    , resize
+    , scale
 ) where
 
-import Hakyll.Images.CompressJpg (compressJpg, compressJpgCompiler, JpgQuality)+import Hakyll.Images.CompressJpg
+import Hakyll.Images.Resize
library/Hakyll/Images/CompressJpg.hs view
@@ -3,7 +3,7 @@ Module      : Hakyll.Images.CompressJpg
 Description : Hakyll compiler to compress Jpeg images
 Copyright   : (c) Laurent P René de Cotret, 2018
-License     : MIT
+License     : BSD3
 Maintainer  : laurent.decotret@outlook.com
 Stability   : stable
 Portability : portable
@@ -39,7 +39,6 @@ 
 import Data.ByteString.Lazy             (ByteString, toStrict)
 
--- From JuicyPixels
 import Codec.Picture.Jpg                (decodeJpeg)
 import Codec.Picture.Saving             (imageToJpg)
 
+ library/Hakyll/Images/Resize.hs view
@@ -0,0 +1,115 @@+
+{-|
+Module      : Hakyll.Images.Resize
+Description : Hakyll compiler to resize images
+Copyright   : (c) Laurent P René de Cotret, 2018
+License     : BSD3
+Maintainer  : laurent.decotret@outlook.com
+Stability   : stable
+Portability : portable
+
+This module defines two Hakyll compilers. The first one, 'resizeImageCompiler', 
+is used to resize images to specific dimensions. The aspect ratio might not be the same.
+
+The other compiler, `scaleImageCompiler`, scales images to fit within a specified 
+box while preserving aspect ratio.
+
+@
+    import Hakyll
+    import Hakyll.Images        (resizeImageCompiler, scaleImageCompiler)
+    
+    (... omitted ...)
+    
+    hakyll $ do
+
+        (... omitted ...)
+        -- Resize all profile pictures to 64x48
+        match "profiles/**.jpg" $ do
+            route idRoute
+            compile (resizeImageCompiler 64 48)
+        
+        -- Scale images to fit within a 600x400 box
+        match "images/**.jpg" $ do
+            route idRoute
+            compile (scaleImageCompiler 600 400)
+        
+        (... omitted ...)
+@
+-}
+module Hakyll.Images.Resize
+    ( Width, Height
+    , resize
+    , resizeImageCompiler
+    , scale
+    , scaleImageCompiler
+    ) where
+
+import Codec.Picture            (convertRGBA8, decodeImage)
+import Codec.Picture.Types
+import Codec.Picture.Saving
+import Codec.Picture.Extra      (scaleBilinear)
+
+import Data.ByteString.Lazy     (ByteString, toStrict)
+import Data.Ratio               ((%))
+
+import Hakyll.Core.Item         (Item(..), itemBody)
+import Hakyll.Core.Compiler     (Compiler, getResourceLBS, getUnderlyingExtension)
+
+type Width = Int
+type Height = Int
+
+-- | Encode a dynamic image to a bytestring based on the file extension
+encode :: String -> DynamicImage -> ByteString
+encode ".jpg" = imageToJpg 100
+encode ".png" = imageToPng
+encode ".bmp" = imageToBitmap
+encode ".tiff" = imageToTiff
+encode fmt = error $ "Unsupported format " <> fmt
+
+-- | Resize an image to specified width and height using the bilinear transform.
+-- The aspect ratio may not be respected.
+--
+-- 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
+
+-- | Compiler that resizes images to a specific dimensions. Aspect ratio
+-- may not be preserved.
+resizeImageCompiler :: Width -> Height -> Compiler (Item ByteString)
+resizeImageCompiler w h = do
+    ext <- getUnderlyingExtension
+    imageItem <- fmap (decodeImage . toStrict) <$> getResourceLBS
+    case itemBody imageItem of
+        Left msg -> error msg
+        Right image -> do
+            let resized = resize w h image
+            return $ Item (itemIdentifier imageItem) (encode ext resized)
+
+-- | Scale an image to a size that will fit in the specified width and height,
+-- while preserving aspect ratio.
+--
+-- In the process, an image is converted to RGBA8. Therefore, some information
+-- loss may occur.
+scale :: Width -> Height -> DynamicImage -> DynamicImage
+scale w h img = resize maxWidth maxHeight img
+    where
+        img' = convertRGBA8 img -- Required to extract height and width
+        (imgWidth, imgHeight) = (imageWidth img', imageHeight img')
+        -- Find the smallest resizing that will accomodate both the width 
+        -- and height.
+        resizing = min (w % imgWidth) (h % imgHeight)
+        maxWidth = round (resizing * fromIntegral imgWidth)
+        maxHeight = round (resizing * fromIntegral imgHeight)
+
+-- | Compiler that rescales images to fit within dimensions. Aspect ratio
+-- will be preserved.
+scaleImageCompiler :: Width -> Height -> Compiler (Item ByteString)
+scaleImageCompiler w h = do
+    ext <- getUnderlyingExtension
+    imageItem <- fmap (decodeImage . toStrict) <$> getResourceLBS
+    case itemBody imageItem of
+        Left msg -> error msg
+        Right image -> do
+            let rescaled = scale w h image
+            return $ Item (itemIdentifier imageItem) (encode ext rescaled)
package.yaml view
@@ -1,5 +1,5 @@ name: hakyll-images-version: '0.0.1'+version: '0.1.0' github: "LaurentRDC/hakyll-images" license: BSD3 author: "Laurent P. René de Cotret"@@ -28,11 +28,13 @@   - base              >= 4.8 && < 5   - bytestring        >= 0.9 && < 0.11   - hakyll            > 4-  - JuicyPixels       > 3 && < 4+  - JuicyPixels       >= 3+  - JuicyPixels-extra > 0.3   source-dirs: library   exposed-modules:   - Hakyll.Images   - Hakyll.Images.CompressJpg+  - Hakyll.Images.Resize  tests:   spec:@@ -44,8 +46,10 @@     - hakyll-images     - tasty             >= 0.11 && < 1.2     - tasty-hunit       >= 0.9  && < 0.11+    - HUnit-approx      >= 1.0.0.0     # Base hakyll-images dependencies     - base              >= 4.8 && < 5     - bytestring        >= 0.9 && < 0.11     - hakyll            > 4-    - JuicyPixels       > 3 && < 4+    - JuicyPixels       >= 3+    - JuicyPixels-extra > 0.3
stack.yaml view
@@ -18,7 +18,7 @@ # # resolver: ./custom-snapshot.yaml # resolver: https://example.com/snapshots/2018-01-01.yaml-resolver: lts-11.7+resolver: lts-12.22  # User packages to be built. # Various formats can be used as shown in the example below.@@ -38,8 +38,7 @@ # Dependency packages to be pulled from upstream that are not in the resolver # using the same syntax as the packages field. # (e.g., acme-missiles-0.3)-extra-deps: -- hakyll-4.12.4.0+# extra-deps: []  # Override default flag values for local packages and extra-deps # flags: {}
+ tests/Hakyll/Images/Resize/Tests.hs view
@@ -0,0 +1,80 @@+--------------------------------------------------------------------------------
+module Hakyll.Images.Resize.Tests
+    ( tests
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Test.Tasty             (TestTree, testGroup)
+import           Test.Tasty.HUnit       (Assertion, assertBool, assertEqual, testCase)
+import           Test.HUnit.Approx      (assertApproxEqual)
+
+
+--------------------------------------------------------------------------------
+import           Hakyll.Images.Resize
+import qualified Data.ByteString        as B
+import           Data.Ratio             ((%))
+
+import           Codec.Picture
+
+import           Text.Printf            (printf)
+
+-- Original test image "piccolo.jpg" has shape 1170 x 647px
+testJpg :: IO DynamicImage
+testJpg = do
+    img <- decodeJpeg <$> B.readFile "tests/data/piccolo.jpg"
+    case img of
+        Left _ -> error "Could not decode test picture piccolo.jpg"
+        Right im -> return im
+
+fromAssertions :: String       -- ^ Name
+               -> [Assertion]  -- ^ Cases
+               -> [TestTree]   -- ^ Result tests
+fromAssertions name =
+    zipWith testCase [printf "[%2d] %s" n name | n <- [1 :: Int ..]]
+
+-- Test that the rescaled image is of the appropriate scale
+testResize :: Assertion
+testResize = do
+    image <- testJpg
+    let scaledImage = resize 48 64 image
+        converted = convertRGBA8 scaledImage
+        (width, height) = ( imageWidth converted
+                          , imageHeight converted
+                          )
+    assertEqual "Image width was not resized properly" width 48 
+    assertEqual "Image height was not resized properly" height 64
+
+-- Test that the rescaled image fits in the appropriate box
+testScale :: Assertion
+testScale = do
+    image <- testJpg
+    let scaledImage = scale 600 400 image
+        converted = convertRGBA8 scaledImage
+        (width, height) = ( imageWidth converted
+                          , imageHeight converted
+                          )
+    assertEqual "Image width was not scaled properly" width 600
+    assertBool "Image height was not scaled property" (height<= 400)
+
+-- Test that the rescaled image has the same aspect ratio
+testScalePreservesAspectRatio :: Assertion
+testScalePreservesAspectRatio = do
+    image <- testJpg
+
+    let initialAspectRatio = (imageWidth $ convertRGBA8 image) % (imageHeight $ convertRGBA8 image)
+        scaledImage = scale 600 400 image
+        finalAspectRatio = (imageWidth $ convertRGBA8 scaledImage) % (imageHeight $ convertRGBA8 scaledImage)
+    
+    assertApproxEqual "Aspect ratio was not preserved" 0.02 initialAspectRatio finalAspectRatio
+        
+
+--------------------------------------------------------------------------------
+tests :: TestTree
+tests = testGroup "Hakyll.Web.Resize.Tests" $ concat
+   [ fromAssertions "rescale" 
+        [ testResize 
+        , testScale
+        , testScalePreservesAspectRatio
+        ] 
+    ]
tests/TestSuite.hs view
@@ -11,8 +11,12 @@ --------------------------------------------------------------------------------
 import qualified Hakyll.Images.CompressJpg.Tests
 
+import qualified Hakyll.Images.Resize.Tests
 
+
 --------------------------------------------------------------------------------
 main :: IO ()
 main = defaultMain $ testGroup "Hakyll"
-    [ Hakyll.Images.CompressJpg.Tests.tests ]+    [ Hakyll.Images.CompressJpg.Tests.tests
+    , Hakyll.Images.Resize.Tests.tests 
+    ]