packages feed

hakyll-images 1.2.2 → 1.3.0

raw patch · 15 files changed

+480/−188 lines, 15 filesdep +containersdep +vectordep ~hakyllbinary-addedPVP ok

version bump matches the API change (PVP)

Dependencies added: containers, vector

Dependency ranges changed: hakyll

API changes (from Hackage documentation)

- Hakyll.Images: compressJpg :: Integral a => a -> Image -> Image
- Hakyll.Images.CompressJpg: compressJpg :: Integral a => a -> Image -> Image
+ Hakyll.Images.Internal: Bitmap :: ImageFormat
+ Hakyll.Images.Internal: Gif :: ImageFormat
+ Hakyll.Images.Internal: Image :: !ImageFormat -> !ByteString -> Image
+ Hakyll.Images.Internal: Jpeg :: ImageFormat
+ Hakyll.Images.Internal: MkWithMetadata :: !a -> !Metadatas -> WithMetadata a
+ Hakyll.Images.Internal: Png :: ImageFormat
+ Hakyll.Images.Internal: Tiff :: ImageFormat
+ Hakyll.Images.Internal: [format] :: Image -> !ImageFormat
+ Hakyll.Images.Internal: [getData] :: WithMetadata a -> !a
+ Hakyll.Images.Internal: [getMetadata] :: WithMetadata a -> !Metadatas
+ Hakyll.Images.Internal: [image] :: Image -> !ByteString
+ Hakyll.Images.Internal: data Image
+ Hakyll.Images.Internal: data ImageFormat
+ Hakyll.Images.Internal: data WithMetadata a
+ Hakyll.Images.Internal: decodeContent :: ByteString -> WithMetadata DynamicImage
+ Hakyll.Images.Internal: encode :: ImageFormat -> ImageContent -> Image
+ Hakyll.Images.Internal: loadImage :: Compiler (Item Image)
+ Hakyll.Images.Internal: type ImageContent = WithMetadata DynamicImage
+ Hakyll.Images.Internal: withImageContent :: (ImageContent -> ImageContent) -> (ImageFormat -> ImageContent -> Image) -> Image -> Image
- Hakyll.Images: ensureFit :: Width -> Height -> DynamicImage -> DynamicImage
+ Hakyll.Images: ensureFit :: Width -> Height -> ImageContent -> ImageContent
- Hakyll.Images: resize :: Width -> Height -> DynamicImage -> DynamicImage
+ Hakyll.Images: resize :: Width -> Height -> ImageContent -> ImageContent
- Hakyll.Images: scale :: Width -> Height -> DynamicImage -> DynamicImage
+ Hakyll.Images: scale :: Width -> Height -> ImageContent -> ImageContent
- Hakyll.Images.Resize: ensureFit :: Width -> Height -> DynamicImage -> DynamicImage
+ Hakyll.Images.Resize: ensureFit :: Width -> Height -> ImageContent -> ImageContent
- Hakyll.Images.Resize: resize :: Width -> Height -> DynamicImage -> DynamicImage
+ Hakyll.Images.Resize: resize :: Width -> Height -> ImageContent -> ImageContent
- Hakyll.Images.Resize: scale :: Width -> Height -> DynamicImage -> DynamicImage
+ Hakyll.Images.Resize: scale :: Width -> Height -> ImageContent -> ImageContent

Files

CHANGELOG.md view
@@ -1,5 +1,13 @@ # Change log +## Release 1.3.0++* Fixed handling of EXIF orientation of metadata, which was previously thought to be fixed (#11).+  The EXIF orientation tag, if it exists, will now persist through all compilers provided+  by `hakyll-images`.+* Removed the `compressJpg` function, which was only used for testing. `compressJpgCompiler` is still here of course.+* Refactoring of the test suite, which led to internals being exposed in `Hakyll.Images.Internal`.+ ## Release 1.2.2  * Minor documentation improvements
hakyll-images.cabal view
@@ -1,9 +1,11 @@ cabal-version: 2.2  name:           hakyll-images-version:        1.2.2+version:        1.3.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.+description:    +  hakyll-images is an add-on to the hakyll package. It adds utilities to work with images, +  including resizing and JPEG compression. category:       Web homepage:       https://github.com/LaurentRDC/hakyll-images#readme bug-reports:    https://github.com/LaurentRDC/hakyll-images/issues@@ -12,7 +14,7 @@ license:        BSD-3-Clause license-file:   LICENSE.md build-type:     Simple-extra-source-files:+extra-doc-files:     CHANGELOG.md     LICENSE.md     README.md@@ -20,6 +22,7 @@     tests/data/piccolo.jpg     tests/data/donkey.gif     tests/data/issue11.jpeg+    tests/data/issue11-2.jpg  source-repository head   type: git@@ -29,23 +32,23 @@   exposed-modules:       Hakyll.Images       Hakyll.Images.CompressJpg+      Hakyll.Images.Internal       Hakyll.Images.Resize       Hakyll.Images.Metadata   other-modules:+      Codec.Picture.Saving.WithMetadata       Hakyll.Images.Common-      Paths_hakyll_images-  autogen-modules:-      Paths_hakyll_images   hs-source-dirs:       library   ghc-options: -Wall -Wcompat   build-depends:-      JuicyPixels       >= 3   && <4-    , JuicyPixels-extra >  0.3 && <1-    , base              >= 4.8 && <5-    , binary            >= 0.5 && <1-    , bytestring        >= 0.9 && <1-    , hakyll            >  4   && <5+      JuicyPixels       >= 3    && <4+    , JuicyPixels-extra >  0.3  && <1+    , base              >= 4.8  && <5+    , binary            >= 0.5  && <1+    , bytestring        >= 0.9  && <1+    , hakyll            >  4    && <5+    , vector            >= 0.12 && <0.14   default-language: Haskell2010  test-suite spec@@ -56,15 +59,8 @@       Hakyll.Images.CompressJpg.Tests       Hakyll.Images.Metadata.Tests       Hakyll.Images.Resize.Tests-      Hakyll.Images-      Hakyll.Images.Common-      Hakyll.Images.CompressJpg-      Hakyll.Images.Metadata-      Hakyll.Images.Resize-      Paths_hakyll_images-  hs-source-dirs:-      tests-      library+      Hakyll.Images.Tests.Utils+  hs-source-dirs: tests   ghc-options: -Wall -Wcompat   build-depends:       HUnit-approx      >= 1    && <2@@ -73,10 +69,12 @@     , base              >= 4.8  && <5     , binary            >= 0.5  && <1     , bytestring        >= 0.9  && <1+    , containers     , directory         >= 1    && <2     , filepath          >= 1    && <2     , hakyll            >= 4    && <5     , hakyll-images     , tasty             >= 0.11 && <2     , tasty-hunit       >= 0.9  && <1+    , vector            >=0.12   default-language: Haskell2010
+ library/Codec/Picture/Saving/WithMetadata.hs view
@@ -0,0 +1,164 @@+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}++-- | Helper functions to save dynamic images to other file format,+-- while preserving metadata when possible.+--+-- This module is modified from `Codec.Picture.Saving` from JuicyPixels+module Codec.Picture.Saving.WithMetadata+  ( imageToJpgWithMetadata,+    imageToPngWithMetadata,+    imageToBitmapWithMetadata,+  )+where++import Codec.Picture.Bitmap (encodeBitmapWithMetadata)+import Codec.Picture.Jpg (encodeDirectJpegAtQualityWithMetadata, encodeJpegAtQualityWithMetadata)+import Codec.Picture.Metadata (Metadatas)+import Codec.Picture.Png (PngSavable (encodePngWithMetadata))+import Codec.Picture.Types+  ( ColorConvertible (promoteImage),+    ColorSpaceConvertible (convertImage),+    DynamicImage (..),+    Image (..),+    Pixel (PixelBaseComponent),+    Pixel8,+    PixelF,+    PixelRGB16,+    PixelRGB8 (..),+    PixelRGBA8,+    PixelRGBF (..),+    dropAlphaLayer,+    pixelMap,+  )+import Data.Bits (unsafeShiftR)+import qualified Data.ByteString.Lazy as L+import qualified Data.Vector.Storable as V+import Data.Word (Word16, Word32, Word8)++componentToLDR :: Float -> Word8+componentToLDR = truncate . (255 *) . min 1.0 . max 0.0++toStandardDef :: Image PixelRGBF -> Image PixelRGB8+toStandardDef = pixelMap pixelConverter+  where+    pixelConverter (PixelRGBF rf gf bf) = PixelRGB8 r g b+      where+        r = componentToLDR rf+        g = componentToLDR gf+        b = componentToLDR bf++greyScaleToStandardDef :: Image PixelF -> Image Pixel8+greyScaleToStandardDef = pixelMap componentToLDR++from16to8 ::+  ( PixelBaseComponent source ~ Word16,+    PixelBaseComponent dest ~ Word8+  ) =>+  Image source ->+  Image dest+from16to8+  Image+    { imageWidth = w,+      imageHeight = h,+      imageData = arr+    } = Image w h transformed+    where+      transformed = V.map toWord8 arr+      toWord8 v = fromIntegral (v `unsafeShiftR` 8)++from32to8 ::+  ( PixelBaseComponent source ~ Word32,+    PixelBaseComponent dest ~ Word8+  ) =>+  Image source ->+  Image dest+from32to8+  Image+    { imageWidth = w,+      imageHeight = h,+      imageData = arr+    } = Image w h transformed+    where+      transformed = V.map toWord8 arr+      toWord8 v = fromIntegral (v `unsafeShiftR` 24)++from32to16 ::+  ( PixelBaseComponent source ~ Word32,+    PixelBaseComponent dest ~ Word16+  ) =>+  Image source ->+  Image dest+from32to16+  Image+    { imageWidth = w,+      imageHeight = h,+      imageData = arr+    } = Image w h transformed+    where+      transformed = V.map toWord16 arr+      toWord16 v = fromIntegral (v `unsafeShiftR` 16)++-- | This function will try to do anything to encode an image+-- as JPEG, make all color conversion and such. Equivalent+-- of 'decodeImage' for jpeg encoding+-- Save Y or YCbCr Jpeg only, all other colorspaces are converted.+-- To save a RGB or CMYK JPEG file, use the+-- 'Codec.Picture.Jpg.Internal.encodeDirectJpegAtQualityWithMetadata' function+imageToJpgWithMetadata :: Int -> Metadatas -> DynamicImage -> L.ByteString+imageToJpgWithMetadata quality meta dynImage =+  let encodeAtQuality = encodeJpegAtQualityWithMetadata (fromIntegral quality) meta+      encodeWithMeta = encodeDirectJpegAtQualityWithMetadata (fromIntegral quality) meta+   in case dynImage of+        ImageYCbCr8 img -> encodeAtQuality img+        ImageCMYK8 img -> imageToJpgWithMetadata quality meta . ImageRGB8 $ convertImage img+        ImageCMYK16 img -> imageToJpgWithMetadata quality meta . ImageRGB16 $ convertImage img+        ImageRGB8 img -> encodeAtQuality (convertImage img)+        ImageRGBF img -> imageToJpgWithMetadata quality meta . ImageRGB8 $ toStandardDef img+        ImageRGBA8 img -> encodeAtQuality (convertImage $ dropAlphaLayer img)+        ImageYF img -> imageToJpgWithMetadata quality meta . ImageY8 $ greyScaleToStandardDef img+        ImageY8 img -> encodeWithMeta img+        ImageYA8 img -> encodeWithMeta $ dropAlphaLayer img+        ImageY16 img -> imageToJpgWithMetadata quality meta . ImageY8 $ from16to8 img+        ImageYA16 img -> imageToJpgWithMetadata quality meta . ImageYA8 $ from16to8 img+        ImageY32 img -> imageToJpgWithMetadata quality meta . ImageY8 $ from32to8 img+        ImageRGB16 img -> imageToJpgWithMetadata quality meta . ImageRGB8 $ from16to8 img+        ImageRGBA16 img -> imageToJpgWithMetadata quality meta . ImageRGBA8 $ from16to8 img++-- | This function will try to do anything to encode an image+-- as PNG, make all color conversion and such. Equivalent+-- of 'decodeImage' for PNG encoding+imageToPngWithMetadata :: Metadatas -> DynamicImage -> L.ByteString+imageToPngWithMetadata meta (ImageYCbCr8 img) = encodePngWithMetadata meta (convertImage img :: Image PixelRGB8)+imageToPngWithMetadata meta (ImageCMYK8 img) = encodePngWithMetadata meta (convertImage img :: Image PixelRGB8)+imageToPngWithMetadata meta (ImageCMYK16 img) = encodePngWithMetadata meta (convertImage img :: Image PixelRGB16)+imageToPngWithMetadata meta (ImageRGB8 img) = encodePngWithMetadata meta img+imageToPngWithMetadata meta (ImageRGBF img) = encodePngWithMetadata meta $ toStandardDef img+imageToPngWithMetadata meta (ImageRGBA8 img) = encodePngWithMetadata meta img+imageToPngWithMetadata meta (ImageY8 img) = encodePngWithMetadata meta img+imageToPngWithMetadata meta (ImageYF img) = encodePngWithMetadata meta $ greyScaleToStandardDef img+imageToPngWithMetadata meta (ImageYA8 img) = encodePngWithMetadata meta img+imageToPngWithMetadata meta (ImageY16 img) = encodePngWithMetadata meta img+imageToPngWithMetadata meta (ImageY32 img) = imageToPngWithMetadata meta . ImageY16 $ from32to16 img+imageToPngWithMetadata meta (ImageYA16 img) = encodePngWithMetadata meta img+imageToPngWithMetadata meta (ImageRGB16 img) = encodePngWithMetadata meta img+imageToPngWithMetadata meta (ImageRGBA16 img) = encodePngWithMetadata meta img++-- | This function will try to do anything to encode an image+-- as bitmap, make all color conversion and such. Equivalent+-- of 'decodeImage' for Bitmap encoding+imageToBitmapWithMetadata :: Metadatas -> DynamicImage -> L.ByteString+imageToBitmapWithMetadata meta (ImageYCbCr8 img) = encodeBitmapWithMetadata meta (convertImage img :: Image PixelRGB8)+imageToBitmapWithMetadata meta (ImageCMYK8 img) = encodeBitmapWithMetadata meta (convertImage img :: Image PixelRGB8)+imageToBitmapWithMetadata meta (ImageCMYK16 img) = imageToBitmapWithMetadata meta . ImageRGB16 $ convertImage img+imageToBitmapWithMetadata meta (ImageRGBF img) = encodeBitmapWithMetadata meta $ toStandardDef img+imageToBitmapWithMetadata meta (ImageRGB8 img) = encodeBitmapWithMetadata meta img+imageToBitmapWithMetadata meta (ImageRGBA8 img) = encodeBitmapWithMetadata meta img+imageToBitmapWithMetadata meta (ImageY8 img) = encodeBitmapWithMetadata meta img+imageToBitmapWithMetadata meta (ImageYF img) = encodeBitmapWithMetadata meta $ greyScaleToStandardDef img+imageToBitmapWithMetadata meta (ImageYA8 img) = encodeBitmapWithMetadata meta (promoteImage img :: Image PixelRGBA8)+imageToBitmapWithMetadata meta (ImageY16 img) = imageToBitmapWithMetadata meta . ImageY8 $ from16to8 img+imageToBitmapWithMetadata meta (ImageY32 img) = imageToBitmapWithMetadata meta . ImageY8 $ from32to8 img+imageToBitmapWithMetadata meta (ImageYA16 img) = imageToBitmapWithMetadata meta . ImageYA8 $ from16to8 img+imageToBitmapWithMetadata meta (ImageRGB16 img) = imageToBitmapWithMetadata meta . ImageRGB8 $ from16to8 img+imageToBitmapWithMetadata meta (ImageRGBA16 img) = imageToBitmapWithMetadata meta . ImageRGBA8 $ from16to8 img
library/Hakyll/Images.hs view
@@ -58,7 +58,6 @@     module Hakyll.Images.Metadata,     -- Jpg compression     JpgQuality,-    compressJpg,     compressJpgCompiler,     -- Image scaling     Width,
library/Hakyll/Images/Common.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-}+ -- | -- Module      : Hakyll.Images.Common -- Description : Types and utilities for Hakyll.Images@@ -11,14 +12,28 @@ -- Portability : portable module Hakyll.Images.Common   ( Image (..),+    withImageContent,+    ImageContent,+    decodeContent,+    WithMetadata (..),     ImageFormat (..),     loadImage,     encode,   ) where +import Codec.Picture (decodeImageWithMetadata)+import Codec.Picture.Metadata (Metadatas)+import qualified Codec.Picture.Metadata as Meta+import qualified Codec.Picture.Metadata.Exif as Meta import Codec.Picture.Saving+import Codec.Picture.Saving.WithMetadata+  ( imageToBitmapWithMetadata,+    imageToJpgWithMetadata,+    imageToPngWithMetadata,+  ) import Codec.Picture.Types (DynamicImage)+import Data.Bifunctor (second) import Data.Binary (Binary (..)) import Data.ByteString (ByteString) import Data.ByteString.Lazy (toStrict)@@ -44,11 +59,39 @@ instance Binary ImageFormat  data Image = Image-  { format :: ImageFormat,-    image :: ByteString+  { format :: !ImageFormat,+    image :: !ByteString   }   deriving (Typeable) +-- Implementation note+-- We need to keep the content of an image as a bytestring, as+-- much as possible, because Hakyll's Items must be serializable.++data WithMetadata a+  = MkWithMetadata+  { getData :: !a,+    getMetadata :: !Metadatas+  }++type ImageContent = WithMetadata DynamicImage++decodeContent :: ByteString -> WithMetadata DynamicImage+decodeContent im = case decodeImageWithMetadata im of+  Left msg -> error msg+  Right content -> uncurry MkWithMetadata (second pruneMetadatas content)+    where+      -- \| Prune metadata to only keep tags which are absolutely necessary+      -- (of which there are very few).+      pruneMetadatas :: Metadatas -> Metadatas+      pruneMetadatas meta =+        foldMap (\(k, v) -> Meta.singleton (Meta.Exif k) v) $+          filter (\(k, _) -> k == Meta.TagOrientation) $+            Meta.extractExifMetas meta++instance Functor WithMetadata where+  fmap f (MkWithMetadata a m) = (MkWithMetadata (f a) m)+ -- When writing to disk, we ignore the image format. -- Trusting users to route correctly. instance Writable Image where@@ -65,8 +108,8 @@ -- -- @ -- match "*.jpg" $ do---     route idRoute---     compile $ loadImage >>= compressJpgCompiler 50+--    route idRoute+--    compile $ loadImage >>= compressJpgCompiler 50 -- @ loadImage :: Compiler (Item Image) loadImage = do@@ -92,9 +135,22 @@     fromExt' ext' = error $ "Unsupported format: " <> ext'  -- Encode images based on file extension-encode :: ImageFormat -> DynamicImage -> Image-encode Jpeg im = Image Jpeg $ (toStrict . imageToJpg 100) im-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 . fromRight (error "Could not parse gif") . imageToGif) im+--+-- Unfortunately, the upstream library `JuicyPixels` does not have support+-- for encoding metadata of some image formats, including `Tiff` and `Gif`.+encode :: ImageFormat -> ImageContent -> Image+encode Jpeg (MkWithMetadata im meta) = Image Jpeg $ (toStrict . imageToJpgWithMetadata 100 meta) im+encode Png (MkWithMetadata im meta) = Image Png $ (toStrict . imageToPngWithMetadata meta) im+encode Bitmap (MkWithMetadata im meta) = Image Bitmap $ (toStrict . imageToBitmapWithMetadata meta) im+encode Tiff (MkWithMetadata im _) = Image Tiff $ (toStrict . imageToTiff) im+encode Gif (MkWithMetadata im _) = Image Gif $ (toStrict . fromRight (error "Could not parse gif") . imageToGif) im++-- | Map over the content of an `Image`, decoded into an `ImageContent`.+withImageContent ::+  (ImageContent -> ImageContent) ->+  -- | Encoder function+  (ImageFormat -> ImageContent -> Image) ->+  (Image -> Image)+withImageContent f encoder (Image fmt bts) =+  let content = decodeContent bts+   in encoder fmt $ f content
library/Hakyll/Images/CompressJpg.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE GeneralisedNewtypeDeriving #-}-{-# LANGUAGE RecordWildCards            #-}-{-# LANGUAGE ScopedTypeVariables        #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+ -- | -- Module      : Hakyll.Images.CompressJpg -- Description : Hakyll compiler to compress Jpeg images@@ -35,90 +36,37 @@ module Hakyll.Images.CompressJpg   ( JpgQuality,     compressJpgCompiler,-    compressJpg,   ) where -import Codec.Picture.Types (DynamicImage(..), dropTransparency, pixelMap)-import qualified Codec.Picture.Types as Picture-import Codec.Picture.Metadata (Metadatas, SourceFormat(SourceJpeg), basicMetadata) -import qualified Codec.Picture.Metadata as Meta-import Codec.Picture.Metadata.Exif (ExifTag(TagOrientation))-import Codec.Picture.Jpg (JpgEncodable, decodeJpegWithMetadata, encodeDirectJpegAtQualityWithMetadata)+import Codec.Picture.Saving.WithMetadata (imageToJpgWithMetadata) import Data.ByteString.Lazy (toStrict)-import Data.ByteString (ByteString)  import Hakyll.Core.Compiler (Compiler) import Hakyll.Core.Item (Item (..)) import Hakyll.Images.Common   ( Image (..),+    ImageContent,     ImageFormat (..),-    format,-    image,+    WithMetadata (..),+    withImageContent,   ) import Numeric.Natural (Natural) - -- | Jpeg encoding quality, from 0 (lower quality) to 100 (best quality). -- @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.------ In the rare case where the JPEG data contains transparency information, it will be dropped.-compressJpg :: Integral a => a -> Image -> Image-compressJpg quality' src =-  if format src /= Jpeg-    then error "Image is not a JPEG."-    -- It is important to preserve some metadata, such as orientation (issue #11).-    else case decodeJpegWithMetadata $ image src of-      Left msg -> error $ "Loading the image failed for the following reason: " <> msg-      Right (dynImage, meta) -> -         Image Jpeg $ case dynImage of -          (ImageY8 img)     -> (encodeJpeg quality meta img)-          (ImageCMYK8 img)  -> (encodeJpeg quality meta img)-          (ImageRGB8 img)   -> (encodeJpeg quality meta img)-          (ImageYCbCr8 img) -> (encodeJpeg quality meta img)-          -- Out of the 5 possible image types that can be returned by `decodeJpegWithMetadata`, only 1-          -- has transparency. This is also the only image type which cannot be re-encoded directly;-          -- we need to remove transparency.-          (ImageYA8 img)    -> (encodeJpeg quality meta (pixelMap dropTransparency img))-          _ -> error "Loading the image failed because the color space is unknown." -  where -    quality = mkJpgQuality quality'----- | Encode a JPEG image at a particular quality, preserving some metadata.-encodeJpeg :: (Integral q, JpgEncodable px) -           => q -           -> Metadatas -           -> Picture.Image px -           -> ByteString-encodeJpeg qual meta img@Picture.Image{..} -  = toStrict $ encodeDirectJpegAtQualityWithMetadata (fromIntegral qual) newmeta img-  where-    -- We want to preserve Exif orientation metadata, which is important for presentation (see #11)-    -- However, other metadata tags are notoriously finicky and can leads to corrupted-    -- files. -    exifOrientationMeta = foldMap (\(k, v) -> Meta.singleton (Meta.Exif k) v) -                        $ filter (\(k, _) -> k == TagOrientation) -                        $ Meta.extractExifMetas meta-    newmeta = exifOrientationMeta <> basicMetadata SourceJpeg imageWidth imageHeight-+mkJpgQuality :: (Integral a) => a -> JpgQuality+mkJpgQuality q+  | q < 0 = JpgQuality 0+  | q > 100 = JpgQuality 100+  | otherwise = JpgQuality (fromIntegral q)  -- | Compiler that compresses a JPG image to a certain quality setting. -- The quality should be between 0 (lowest quality) and 100 (best quality).@@ -127,8 +75,16 @@ -- -- @ -- match "*.jpg" $ do---     route idRoute---     compile $ loadImage >>= compressJpgCompiler 50+--    route idRoute+--    compile $ loadImage >>= compressJpgCompiler 50 -- @-compressJpgCompiler :: Integral a => a -> Item Image -> Compiler (Item Image)-compressJpgCompiler quality = return . fmap (compressJpg quality)+compressJpgCompiler :: (Integral a) => a -> Item Image -> Compiler (Item Image)+compressJpgCompiler quality =+  return . fmap (withImageContent id encoder) -- JPG compression isn't a transformation of the image, but rather a re-encoding+  where+    validatedQuality :: Int+    validatedQuality = fromIntegral $ mkJpgQuality quality++    encoder :: ImageFormat -> ImageContent -> Image+    encoder _ (MkWithMetadata d md) =+      Image Jpeg (toStrict $ imageToJpgWithMetadata validatedQuality md d)
+ library/Hakyll/Images/Internal.hs view
@@ -0,0 +1,16 @@+-- |+-- Module      : Hakyll.Images.Internal+-- Description : Hakyll utilities for image files+-- Copyright   : (c) Laurent P René de Cotret, 2019 - present+-- License     : BSD3+-- Maintainer  : laurent.decotret@outlook.com+-- Stability   : unstable+-- Portability : portable+--+-- Internal re-exports used for tests.+module Hakyll.Images.Internal+  ( module Hakyll.Images.Common,+  )+where++import Hakyll.Images.Common
library/Hakyll/Images/Resize.hs view
@@ -46,31 +46,25 @@   ) where -import Codec.Picture (convertRGBA8, decodeImage)+import Codec.Picture (convertRGBA8) import Codec.Picture.Extra (scaleBilinear) import Codec.Picture.Types (DynamicImage (..), imageHeight, imageWidth)-import Data.ByteString (ByteString) import Data.Ratio ((%)) import Hakyll.Core.Compiler (Compiler) import Hakyll.Core.Item (Item (..))-import Hakyll.Images.Common (Image (..), encode)+import Hakyll.Images.Common (Image (..), ImageContent, WithMetadata (..), encode, withImageContent)  type Width = Int  type Height = Int -decodeImage' :: ByteString -> DynamicImage-decodeImage' im = case decodeImage im of-  Left msg -> error msg-  Right im' -> im'- -- | 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+resize :: Width -> Height -> ImageContent -> ImageContent+resize w h = fmap (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.@@ -79,7 +73,7 @@ -- loss may occur. -- -- To scale images down only, take a look at 'ensureFit'.-scale :: Width -> Height -> DynamicImage -> DynamicImage+scale :: Width -> Height -> ImageContent -> ImageContent scale w h = scale' w h True  -- | Scale an image to a size that will fit in the specified width and height,@@ -95,10 +89,10 @@   -- | Allow scaling up as well.   Bool ->   -- | Source image-  DynamicImage ->+  ImageContent ->   -- | Scaled image.-  DynamicImage-scale' w h upAllowed img = resize maxWidth maxHeight img+  ImageContent+scale' w h upAllowed content@(MkWithMetadata img _) = resize maxWidth maxHeight content   where     img' = convertRGBA8 img -- Required to extract height and width     (imgWidth, imgHeight) = (imageWidth img', imageHeight img')@@ -119,7 +113,7 @@ -- loss may occur. -- -- To scale images up __or__ down, take a look at 'scale'.-ensureFit :: Width -> Height -> DynamicImage -> DynamicImage+ensureFit :: Width -> Height -> ImageContent -> ImageContent ensureFit w h = scale' w h False  -- | Compiler that resizes images to a specific dimensions. Aspect ratio@@ -127,45 +121,39 @@ -- -- @ -- match "*.png" $ do---     route idRoute---     compile $ loadImage >>= resizeImageCompiler 48 64+--    route idRoute+--    compile $ loadImage >>= resizeImageCompiler 48 64 -- @ -- -- Note that in the resizing process, images will be converted to RGBA8. -- To preserve aspect ratio, take a look at 'scaleImageCompiler'. 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+resizeImageCompiler w h = return . fmap (withImageContent (resize w h) encode)  -- | Compiler that rescales images to fit within dimensions. Aspect ratio -- will be preserved. Images might be scaled up as well. -- -- @ -- match "*.tiff" $ do---     route idRoute---     compile $ loadImage >>= scaleImageCompiler 48 64+--    route idRoute+--    compile $ loadImage >>= scaleImageCompiler 48 64 -- @ -- -- Note that in the resizing process, images will be converted to RGBA8. -- To ensure images are only scaled __down__, take a look at 'ensureFitCompiler'. 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+scaleImageCompiler w h = return . fmap (withImageContent (scale w h) encode)  -- | Compiler that ensures images will fit within dimensions. Images might -- be scaled down, but never up.  Aspect ratio will be preserved. -- -- @ -- match "*.tiff" $ do---     route idRoute---     compile $ loadImage >>= ensureFitCompiler 48 64+--    route idRoute+--    compile $ loadImage >>= ensureFitCompiler 48 64 -- @ -- -- Note that in the resizing process, images will be converted to RGBA8. -- To allow the possibility of scaling up, take a look at 'scaleImageCompiler'. 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+ensureFitCompiler w h = return . fmap (withImageContent (ensureFit w h) encode)
tests/Hakyll/Images/Common/Tests.hs view
@@ -14,7 +14,7 @@ import qualified Hakyll.Core.Logger as L import Hakyll.Core.Rules.Internal (RuleSet) import Hakyll.Core.Runtime-import Hakyll.Images+import Hakyll.Images ( loadImage, compressJpgCompiler ) import System.Directory (doesFileExist) import System.Exit      (ExitCode) import System.FilePath ((</>))
tests/Hakyll/Images/CompressJpg/Tests.hs view
@@ -1,18 +1,17 @@---------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+ module Hakyll.Images.CompressJpg.Tests   ( tests,   ) where ---------------------------------------------------------------------------------------------------------------------------------------------------------------------import Codec.Picture (dynamicMap, imageWidth, imageHeight, imageHeight)+import Codec.Picture (dynamicMap, imageHeight, imageWidth) import Codec.Picture.Jpg (decodeJpeg) import qualified Data.ByteString as B-import Hakyll.Images.Common ( Image(Image, image), ImageFormat(Jpeg) )-import Hakyll.Images.CompressJpg ( compressJpg )+import Hakyll (Identifier, Item (..))+import Hakyll.Images.Internal (Image (Image, image), ImageFormat (Jpeg), loadImage)+import Hakyll.Images.CompressJpg (compressJpgCompiler)+import Hakyll.Images.Tests.Utils (testCompilerDone) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, testCase) import Text.Printf (printf)@@ -30,55 +29,58 @@ fromAssertions name =   zipWith testCase [printf "[%2d] %s" n name | n <- [1 :: Int ..]] +compressJpg :: (Integral a) => a -> Identifier -> IO Image+compressJpg quality idt = testCompilerDone idt $ do+  Item _ compressed <- loadImage >>= compressJpgCompiler quality+  return $ compressed+ -- Test that the standard Image compressed to quality 25/100 is smaller -- than the initial image testCompressionFromImage :: Assertion testCompressionFromImage = do   im <- testJpg   let initialSize = (B.length . image) im-      finalSize = (B.length . image . compressJpg (25::Int)) im+  finalSize <- do+    testCompilerDone "piccolo.jpg" $ do+      Item _ compressed <- loadImage >>= compressJpgCompiler (25 :: Int)+      return $ (B.length . image) compressed    assertBool "Image was not compressed" (initialSize > finalSize)  -- Test that specifying a JPG encoding below 0 will not fail testJpgEncodingOutOfLowerBound :: Assertion testJpgEncodingOutOfLowerBound = do-  im <- testJpg-  -  let compressedSize = (B.length . image . compressJpg (-10 :: Int)) im-      expectedSize = (B.length . image . compressJpg (0 :: Int)) im+  compressedSize <- B.length . image <$> compressJpg (-10 :: Int) "piccolo.jpg"+  expectedSize <- B.length . image <$> compressJpg (0 :: Int) "piccolo.jpg"    assertBool "Out-of-bounds JpgQuality was not handled properly" (expectedSize == compressedSize)  -- Test that specifying a JPG encoding above 100 will fail testJpgEncodingOutOfUpperBound :: Assertion testJpgEncodingOutOfUpperBound = do-  im <- testJpg-  -  let compressedSize = (B.length . image . compressJpg (150 :: Int)) im-      expectedSize = (B.length . image . compressJpg (100 :: Int)) im+  compressedSize <- B.length . image <$> compressJpg (150 :: Int) "piccolo.jpg"+  expectedSize <- B.length . image <$> compressJpg (100 :: Int) "piccolo.jpg"    assertBool "Out-of-bounds JpgQuality was not handled properly" (expectedSize == compressedSize)  -- Test that issue #11 is fixed testJpgInadvertentRotation :: TestTree testJpgInadvertentRotation = testCase "Compressing a JPEG does not rotate it (issue #11)" $ do-  im <- Image Jpeg <$> B.readFile "tests/data/issue11.jpeg"+  im <- B.readFile "tests/data/issue11.jpeg" -  let initial    = either error id $ decodeJpeg (image im)-      compressed = either error id $ decodeJpeg $ image $ compressJpg (50::Int) im+  let initial = either error id $ decodeJpeg im+  compressed <- either error id <$> (compressJpg (50 :: Int) "issue11.jpeg" >>= pure . decodeJpeg . image)    assertEqual mempty (dynamicMap imageWidth initial) (dynamicMap imageWidth compressed)   assertEqual mempty (dynamicMap imageHeight initial) (dynamicMap imageHeight compressed) --------------------------------------------------------------------------------- tests :: TestTree tests =   testGroup "Hakyll.Images.CompressJpg.Tests" $-    [testJpgInadvertentRotation] <>-      fromAssertions-          "compressJpg"-          [ testCompressionFromImage,-            testJpgEncodingOutOfLowerBound,-            testJpgEncodingOutOfUpperBound-          ]+    [testJpgInadvertentRotation]+      <> fromAssertions+        "compressJpg"+        [ testCompressionFromImage,+          testJpgEncodingOutOfLowerBound,+          testJpgEncodingOutOfUpperBound+        ]
tests/Hakyll/Images/Metadata/Tests.hs view
@@ -12,7 +12,7 @@ import qualified Data.ByteString as B import Data.Maybe (fromMaybe) import Hakyll.Images-import Hakyll.Images.Common+import Hakyll.Images.Internal import qualified Hakyll.Images.Metadata as M import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (Assertion, assertBool, testCase)
tests/Hakyll/Images/Resize/Tests.hs view
@@ -1,36 +1,34 @@---------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+ module Hakyll.Images.Resize.Tests   ( tests,   ) where -------------------------------------------------------------------------------------------------------------------------------------------------------------------- import Codec.Picture+  ( convertRGBA8,+    decodeJpeg,+    dynamicMap,+    imageHeight,+    imageWidth,+  ) import qualified Data.ByteString as B import Data.Ratio ((%))+import Hakyll (Item (Item)) import Hakyll.Images+import Hakyll.Images.Internal (Image (Image), ImageContent, WithMetadata (..), decodeContent)+import Hakyll.Images.Tests.Utils import Test.HUnit.Approx (assertApproxEqual) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, testCase) 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+testJpg :: IO ImageContent+testJpg = decodeContent <$> B.readFile "tests/data/piccolo.jpg" -testGif :: IO DynamicImage-testGif = do-  img <- decodeGif <$> B.readFile "tests/data/donkey.gif"-  case img of-    Left _ -> error "Could not decode test picture donkey.gif"-    Right im -> return im+testGif :: IO ImageContent+testGif = decodeContent <$> B.readFile "tests/data/donkey.gif"  fromAssertions ::   -- | Name@@ -47,7 +45,7 @@ testResizeJpg = do   image <- testJpg   let scaledImage = resize 48 64 image-      converted = convertRGBA8 scaledImage+      MkWithMetadata converted _ = fmap convertRGBA8 scaledImage       (width, height) =         ( imageWidth converted,           imageHeight converted@@ -60,7 +58,7 @@ testResizeGif = do   image <- testGif   let scaledImage = resize 48 64 image-      converted = convertRGBA8 scaledImage+      MkWithMetadata converted _ = fmap convertRGBA8 scaledImage       (width, height) =         ( imageWidth converted,           imageHeight converted@@ -73,7 +71,7 @@ testScale = do   image <- testJpg   let scaledImage = scale 600 400 image-      converted = convertRGBA8 scaledImage+      MkWithMetadata converted _ = fmap convertRGBA8 scaledImage       (width, height) =         ( imageWidth converted,           imageHeight converted@@ -86,9 +84,9 @@ testEnsureFit :: Assertion testEnsureFit = do   image <- testJpg-  let (originalWidth, originalHeight) = (imageWidth . convertRGBA8 $ image, imageHeight . convertRGBA8 $ image)+  let (originalWidth, originalHeight) = (imageWidth . convertRGBA8 . getData $ image, imageHeight . convertRGBA8 . getData $ image)       scaledImage = ensureFit 2000 2000 image-      converted = convertRGBA8 scaledImage+      MkWithMetadata converted _ = fmap convertRGBA8 scaledImage       (width, height) =         ( imageWidth converted,           imageHeight converted@@ -101,13 +99,36 @@ testScalePreservesAspectRatio = do   image <- testJpg -  let initialAspectRatio = imageWidth (convertRGBA8 image) % imageHeight (convertRGBA8 image)+  let initialAspectRatio = imageWidth (convertRGBA8 $ getData image) % imageHeight (convertRGBA8 $ getData image)       scaledImage = scale 600 400 image-      finalAspectRatio = imageWidth (convertRGBA8 scaledImage) % imageHeight (convertRGBA8 scaledImage)+      finalAspectRatio = imageWidth (convertRGBA8 $ getData scaledImage) % imageHeight (convertRGBA8 $ getData scaledImage)    assertApproxEqual "Aspect ratio was not preserved" 0.02 initialAspectRatio finalAspectRatio ---------------------------------------------------------------------------------+-- Test that issue #11 is fixed+testJpgInadvertentRotation :: TestTree+testJpgInadvertentRotation = testCase "resizing a JPEG does not rotate it (issue #11)" $ do+  Item _ (Image _ raw) <-+    testCompilerDone "issue11-2.jpg" $ loadImage++  let original = either error id $ decodeJpeg raw+      initialWidth = dynamicMap imageWidth original+      initialHeight = dynamicMap imageHeight original++  Item _ (Image _ bts) <-+    testCompilerDone "issue11-2.jpg" $ loadImage >>= ensureFitCompiler 400 400++  let resized = either error id $ decodeJpeg bts+      measuredWidth = dynamicMap imageWidth resized+      measuredHeight = dynamicMap imageHeight resized++  -- The image *appears* to be w=300, h=400,+  -- but an exif tag that rotates it means that we expect+  -- the height to be smaller than the width+  assertBool mempty (initialHeight < initialWidth)+  -- The same must hold for the resized image+  assertBool mempty (measuredHeight < measuredWidth)+ tests :: TestTree tests =   testGroup "Hakyll.Images.Resize.Tests" $@@ -119,8 +140,10 @@           ],         fromAssertions           "ensureFit"-          [testEnsureFit],+          [ testEnsureFit+          ],         fromAssertions           "resize"-          [testResizeJpg, testResizeGif]+          [testResizeJpg, testResizeGif],+        [testJpgInadvertentRotation]       ]
+ tests/Hakyll/Images/Tests/Utils.hs view
@@ -0,0 +1,79 @@+-- | Test utilities+module Hakyll.Images.Tests.Utils+  ( testCompiler,+    testCompilerDone,+    testConfiguration,+    cleanTestEnv,+  )+where++import Data.List (intercalate)+import qualified Data.Set as S+import Hakyll.Core.Compiler.Internal+import Hakyll.Core.Configuration+import Hakyll.Core.Identifier+import qualified Hakyll.Core.Logger as Logger+import Hakyll.Core.Provider+import qualified Hakyll.Core.Store as Store+import Hakyll.Core.Util.File++testCompiler ::+  Identifier ->+  Compiler a ->+  IO (CompilerResult a)+testCompiler underlying compiler = do+  store <- Store.new True $ storeDirectory testConfiguration+  provider <-+    newProvider store (const $ return False) $+      providerDirectory testConfiguration+  logger <- Logger.new Logger.Error+  let read' =+        CompilerRead+          { compilerConfig = testConfiguration,+            compilerUnderlying = underlying,+            compilerProvider = provider,+            compilerUniverse = S.empty,+            compilerRoutes = mempty,+            compilerStore = store,+            compilerLogger = logger+          }++  result <- runCompiler compiler read'+  Logger.flush logger+  return result++testCompilerDone :: Identifier -> Compiler a -> IO a+testCompilerDone underlying compiler = do+  result <- testCompiler underlying compiler+  case result of+    CompilerDone x _ -> return x+    CompilerError e ->+      fail $+        "Hakyll.Images.Tests.Utils.testCompilerDone: compiler "+          ++ show underlying+          ++ " threw: "+          ++ intercalate "; " (compilerErrorMessages e)+    CompilerRequire i _ ->+      fail $+        "Hakyll.Images.Tests.Utils.testCompilerDone: compiler "+          ++ show underlying+          ++ " requires: "+          ++ show i+    CompilerSnapshot _ _ ->+      fail+        "Hakyll.Images.Tests.Utils.testCompilerDone: unexpected CompilerSnapshot"++testConfiguration :: Configuration+testConfiguration =+  defaultConfiguration+    { destinationDirectory = "_testsite",+      storeDirectory = "_teststore",+      tmpDirectory = "_testtmp",+      providerDirectory = "tests/data"+    }++cleanTestEnv :: IO ()+cleanTestEnv = do+  removeDirectory $ destinationDirectory testConfiguration+  removeDirectory $ storeDirectory testConfiguration+  removeDirectory $ tmpDirectory testConfiguration
tests/TestSuite.hs view
@@ -12,11 +12,12 @@ import qualified Hakyll.Images.CompressJpg.Tests import qualified Hakyll.Images.Metadata.Tests import qualified Hakyll.Images.Resize.Tests+import qualified Hakyll.Images.Tests.Utils import Test.Tasty (defaultMain, testGroup)  -------------------------------------------------------------------------------- main :: IO ()-main =+main = do   defaultMain $     testGroup       "Hakyll"@@ -25,3 +26,5 @@         Hakyll.Images.Resize.Tests.tests,         Hakyll.Images.Metadata.Tests.tests       ]++  Hakyll.Images.Tests.Utils.cleanTestEnv
+ tests/data/issue11-2.jpg view

binary file changed (absent → 1025881 bytes)