diff --git a/cabal.project b/cabal.project index c9b8b11722..710ac916ea 100644 --- a/cabal.project +++ b/cabal.project @@ -63,3 +63,9 @@ source-repository-package location: https://github.com/simplex-chat/wai.git tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0 subdir: warp + +source-repository-package + type: git + location: https://gitlab.com/dpwiz/hs-jpeg-turbo + tag: bd65878bd29933c7cee262c3b7bf82186a32812c + subdir: JuicyPixels-jpeg-turbo jpeg-turbo diff --git a/package.yaml b/package.yaml index 135e8ed80b..20de459f27 100644 --- a/package.yaml +++ b/package.yaml @@ -51,6 +51,7 @@ dependencies: - uuid == 1.3.* - zip == 2.0.* - JuicyPixels + - JuicyPixels-jpeg-turbo - JuicyPixels-stbir flags: diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 865561fefd..19d9172925 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -200,6 +200,7 @@ library ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns build-depends: JuicyPixels + , JuicyPixels-jpeg-turbo , JuicyPixels-stbir , aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -265,6 +266,7 @@ executable simplex-bot ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: JuicyPixels + , JuicyPixels-jpeg-turbo , JuicyPixels-stbir , aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -331,6 +333,7 @@ executable simplex-bot-advanced ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: JuicyPixels + , JuicyPixels-jpeg-turbo , JuicyPixels-stbir , aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -400,6 +403,7 @@ executable simplex-broadcast-bot ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: JuicyPixels + , JuicyPixels-jpeg-turbo , JuicyPixels-stbir , aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -467,6 +471,7 @@ executable simplex-chat ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: JuicyPixels + , JuicyPixels-jpeg-turbo , JuicyPixels-stbir , aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -540,6 +545,7 @@ executable simplex-directory-service ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: JuicyPixels + , JuicyPixels-jpeg-turbo , JuicyPixels-stbir , aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -640,6 +646,7 @@ test-suite simplex-chat-test ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: JuicyPixels + , JuicyPixels-jpeg-turbo , JuicyPixels-stbir , QuickCheck ==2.14.* , aeson ==2.2.* diff --git a/src/Simplex/Chat/Image.hs b/src/Simplex/Chat/Image.hs index 61808bc877..7047630b39 100644 --- a/src/Simplex/Chat/Image.hs +++ b/src/Simplex/Chat/Image.hs @@ -10,7 +10,7 @@ module Simplex.Chat.Image where import qualified Codec.Picture as Picture -import Codec.Picture.Jpg (encodeDirectJpegAtQualityWithMetadata) +import qualified Codec.Picture.JPEGTurbo as JT import Codec.Picture.Metadata (Metadatas) import Codec.Picture.Png (encodePng) import qualified Codec.Picture.STBIR as STBIR @@ -21,12 +21,12 @@ import qualified Data.ByteString.Base64.Lazy as LB64 import qualified Data.ByteString.Lazy.Char8 as LB import Data.Int (Int64) import Data.Monoid (All (..)) -import Data.Word (Word8) +import System.IO.Unsafe (unsafePerformIO) -resizeImageToSize :: Bool -> Int64 -> ResizeableImage -> LB.ByteString -resizeImageToSize toURI maxSize (ResizeableImage fmt img encoder) = either resizePNG resizeJPG encoder +resizeImageToSize :: Bool -> Int -> Int64 -> ResizeableImage -> LB.ByteString +resizeImageToSize toURI minJpegQuality maxSize (ResizeableImage fmt img encoder) = either resizePNG resizeJPG encoder where - halveAndRetry = resizeImageToSize toURI maxSize $ ResizeableImage fmt imgHalved encoder + halveAndRetry = resizeImageToSize toURI minJpegQuality maxSize (ResizeableImage fmt imgHalved encoder) imgHalved = downscale img 2.0 resizePNG enc | LB.length encoded <= maxSize = encoded @@ -49,12 +49,12 @@ resizeImageToSize toURI maxSize (ResizeableImage fmt img encoder) = either resiz result = encode $ downscale img m resizeJPG enc | minSize > maxSize = halveAndRetry - | otherwise = fitQuality 50 99 + | otherwise = fitQuality minJpegQuality 95 where encode q | toURI = toDataUri "jpg" $ enc q img -- the correct mime type is "jpeg", but only "jpg" is supported by older clients | otherwise = enc q img - minSize = LB.length $ encode 50 + minSize = LB.length $ encode minJpegQuality fitQuality l u | u - l <= 1 = encode l -- prefer higher compression | otherwise = @@ -107,21 +107,21 @@ data ResizeableImage where type ImageEncoder a = Either (PNGEncoder a) (JPGEncoder a) type PNGEncoder a = Picture.Image a -> LB.ByteString -type JPGEncoder a = Word8 -> Picture.Image a -> LB.ByteString +type JPGEncoder a = Int -> Picture.Image a -> LB.ByteString resizeableImage :: Picture.DynamicImage -> Maybe ResizeableImage resizeableImage dyn = case dyn of - Picture.ImageY8 img -> Just $ ResizeableImage "Y8" img $ Right $ \q -> encodeDirectJpegAtQualityWithMetadata q mempty + Picture.ImageY8 img -> Just $ ResizeableImage "Y8" img $ Right $ \q -> LB.fromStrict . unsafePerformIO . JT.encodeRGB q . Picture.convertRGB8 . Picture.ImageY8 -- TODO: directly Picture.ImageY16 img -> Just $ ResizeableImage "Y16" img $ Left encodePng Picture.ImageY32 _ -> Nothing Picture.ImageYF _ -> Nothing Picture.ImageYA8 img -> Just $ ResizeableImage "YA8" img $ Left encodePng Picture.ImageYA16 img -> Just $ ResizeableImage "YA16" img $ Left encodePng - Picture.ImageRGB8 img -> Just $ ResizeableImage "RGB8" img $ Right $ \q -> encodeDirectJpegAtQualityWithMetadata q mempty + Picture.ImageRGB8 img -> Just $ ResizeableImage "RGB8" img $ Right $ \q -> LB.fromStrict . unsafePerformIO . JT.encodeRGB q Picture.ImageRGB16 img -> Just $ ResizeableImage "RGB16" img $ Left encodePng Picture.ImageRGBF _ -> Nothing Picture.ImageRGBA8 img -> Just $ ResizeableImage "RGBA8" img $ Left encodePng Picture.ImageRGBA16 img -> Just $ ResizeableImage "RGBA16" img $ Left encodePng - Picture.ImageYCbCr8 img -> Just $ ResizeableImage "YCbCr8" img $ Right $ \q -> encodeDirectJpegAtQualityWithMetadata q mempty - Picture.ImageCMYK8 img -> Just $ ResizeableImage "CMYK8" img $ Right $ \q -> encodeDirectJpegAtQualityWithMetadata q mempty + Picture.ImageYCbCr8 img -> Just $ ResizeableImage "YCbCr8" img $ Right $ \q -> LB.fromStrict . unsafePerformIO . JT.encodeRGB q . Picture.convertRGB8 . Picture.ImageYCbCr8 -- TODO: JT.encodeYUV + Picture.ImageCMYK8 img -> Just $ ResizeableImage "CMYK8" img $ Right $ \q -> LB.fromStrict . unsafePerformIO . JT.encodeRGB q . Picture.convertRGB8 . Picture.ImageCMYK8 Picture.ImageCMYK16 _ -> Nothing diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 57b28e1850..4a881a59ca 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -210,9 +210,11 @@ cChatResizeImageToStrSize fp' maxSize = do fp <- peekCString fp' res <- runExceptT $ do (ri, _) <- liftIOEither $ readResizeable fp - let resized = resizeImageToSize True (fromIntegral maxSize) ri + let resized = resizeImageToSize True previewMinQuality (fromIntegral maxSize) ri if LB.length resized > fromIntegral maxSize then throwError "unable to fit" else pure resized newCStringFromLazyBS $ fromRight "" res + where + previewMinQuality = 20 -- -- | Strip EXIF etc metadata from image, inlplace -- cChatStripImageMetadata :: CString -> IO CBool diff --git a/src/Simplex/Chat/Mobile/File.hs b/src/Simplex/Chat/Mobile/File.hs index a304a79c9c..3b0a154895 100644 --- a/src/Simplex/Chat/Mobile/File.hs +++ b/src/Simplex/Chat/Mobile/File.hs @@ -82,11 +82,13 @@ cChatWriteImage cc maxSize cPath ptr len encrypt = do case Picture.decodeResizeable src of Left e -> pure $ WFError e Right (ri, _metadata) -> do - let resized = resizeImageToSize False (fromIntegral maxSize) ri + let resized = resizeImageToSize False storeMinQuality (fromIntegral maxSize) ri if LB.length resized > fromIntegral maxSize then pure $ WFError "unable to fit" else chatWriteFile_ cfArgs_ path (LB.toStrict resized) newCStringFromLazyBS $ J.encode r + where + storeMinQuality = 20 data ReadFileResult = RFResult {fileSize :: Int} diff --git a/tests/LinkPreviewTests.hs b/tests/LinkPreviewTests.hs index b06cb3bcad..2a22738115 100644 --- a/tests/LinkPreviewTests.hs +++ b/tests/LinkPreviewTests.hs @@ -21,7 +21,7 @@ resizeToStrTest :: FilePath -> FilePath -> IO () resizeToStrTest inputPath tmp = do (ri@(ResizeableImage imgFormat _img encoder), metadata) <- either error pure =<< Image.readResizeable inputPath logDebug $ tshow (metadata, imgFormat, either (const "png") (const "jpg") encoder) - let res = Image.resizeImageToSize True maxSize ri + let res = Image.resizeImageToSize True 20 maxSize ri finalSize = LB.length res unless (finalSize <= maxSize) $ error $ "Final size larger than maximum size: " <> show (finalSize, maxSize) let (fmt, b64) = fmap (LB.drop 8) . LB.break (== ';') $ LB.drop 11 res