diff --git a/src/Simplex/Chat/Image.hs b/src/Simplex/Chat/Image.hs index 0677462cf1..776b0cdb0a 100644 --- a/src/Simplex/Chat/Image.hs +++ b/src/Simplex/Chat/Image.hs @@ -9,30 +9,33 @@ module Simplex.Chat.Image where -import qualified Data.ByteString.Base64.Lazy as LB64 -import qualified Data.ByteString.Lazy.Char8 as LB -import Control.Monad.Except (ExceptT (..), runExceptT, throwError) import qualified Codec.Picture as Picture -import Codec.Picture.Metadata (Metadatas) import Codec.Picture.Jpg (encodeDirectJpegAtQualityWithMetadata) +import Codec.Picture.Metadata (Metadatas) import Codec.Picture.Png (encodePng) import qualified Codec.Picture.STBIR as STBIR import Codec.Picture.Types (dropAlphaLayer, pixelFoldMap) +import Control.Monad.Except (ExceptT (..), runExceptT, throwError) +import Data.ByteString (ByteString) +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.Monoid (All (..)) import Data.Word (Word8) -resizeImageToStrSize :: Int64 -> ResizeableImage -> LB.ByteString -resizeImageToStrSize maxSize (ResizeableImage fmt img encoder) = either resizePNG resizeJPG encoder +resizeImageToSize :: Bool -> Int64 -> ResizeableImage -> LB.ByteString +resizeImageToSize toURI maxSize (ResizeableImage fmt img encoder) = either resizePNG resizeJPG encoder where - halveAndRetry = resizeImageToStrSize maxSize $ ResizeableImage fmt imgHalved encoder + halveAndRetry = resizeImageToSize toURI maxSize $ ResizeableImage fmt imgHalved encoder imgHalved = downscale img 2.0 resizePNG enc | LB.length encoded <= maxSize = encoded | LB.length (encode imgHalved) > maxSize = halveAndRetry | otherwise = fitScale 1.0 2.0 where - encode = toDataUri "png" enc + encode + | toURI = toDataUri "png" . enc + | otherwise = enc encoded = encode img fitScale l u | u - l < 1 / 64 = encode $ downscale img u -- prefer lower resolution @@ -41,14 +44,16 @@ resizeImageToStrSize maxSize (ResizeableImage fmt img encoder) = either resizePN LT -> fitScale m u -- over budget, scale harder EQ -> result GT -> fitScale l m -- keep more pixels - where - m = (l + u) / 2 - result = encode $ downscale img m + where + m = (l + u) / 2 + result = encode $ downscale img m resizeJPG enc | minSize > maxSize = halveAndRetry | otherwise = fitQuality 33 99 where - encode q = toDataUri "jpeg" (enc q) img + encode q + | toURI = toDataUri "jpeg" $ enc q img + | otherwise = enc q img minSize = LB.length $ encode 33 fitQuality l u | u - l <= 1 = encode l -- prefer higher compression @@ -57,21 +62,26 @@ resizeImageToStrSize maxSize (ResizeableImage fmt img encoder) = either resizePN LT -> fitQuality m u -- keep more data EQ -> result GT -> fitQuality l m -- over budget, reduce quality - where - m = (l + u) `div` 2 - result = encode m + where + m = (l + u) `div` 2 + result = encode m downscale :: STBIR.STBIRPixel pixel => Picture.Image pixel -> Float -> Picture.Image pixel downscale img scale = STBIR.resize STBIR.defaultOptions (scaled Picture.imageWidth) (scaled Picture.imageHeight) img where scaled f = round $ fromIntegral (f img) / min 2.0 (max 0.5 scale) -toDataUri :: LB.ByteString -> (Picture.Image a -> LB.ByteString) -> Picture.Image a -> LB.ByteString -toDataUri fmt enc img = "data:image/" <> fmt <> ";base64," <> LB64.encode (enc img) +toDataUri :: LB.ByteString -> LB.ByteString -> LB.ByteString +toDataUri fmt body = "data:image/" <> fmt <> ";base64," <> LB64.encode body + +decodeResizeable :: ByteString -> Either String (ResizeableImage, Metadatas) +decodeResizeable source = do + (input, metadata) <- Picture.decodeImageWithMetadata source + maybe (Left "unsupported image format") (pure . (,metadata)) $ resizeableImage (tryDropOpacity input) readResizeable :: FilePath -> IO (Either String (ResizeableImage, Metadatas)) readResizeable inputPath = runExceptT $ do - (input, metadata) <- ExceptT $ Picture.readImageWithMetadata inputPath + (input, metadata) <- ExceptT $ Picture.readImageWithMetadata inputPath -- will use mmap instead of reading the whole file maybe (throwError "unsupported image format") (pure . (,metadata)) $ resizeableImage (tryDropOpacity input) tryDropOpacity :: Picture.DynamicImage -> Picture.DynamicImage @@ -86,12 +96,14 @@ tryDropOpacity dyn = case dyn of opaque = getAll . pixelFoldMap (All . \pix -> Picture.pixelOpacity pix == maxBound) data ResizeableImage where - ResizeableImage - :: (STBIR.STBIRPixel a) -- , ToDynamicImage a) - => String -- ^ format label - -> Picture.Image a -- ^ current image data - -> ImageEncoder a - -> ResizeableImage + ResizeableImage :: + STBIR.STBIRPixel a => + -- | format label + String -> + -- | current image data + Picture.Image a -> + ImageEncoder a -> + ResizeableImage type ImageEncoder a = Either (PNGEncoder a) (JPGEncoder a) type PNGEncoder a = Picture.Image a -> LB.ByteString @@ -113,4 +125,3 @@ resizeableImage dyn = case dyn of 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.ImageCMYK16 _ -> Nothing - -- STBIR says all types currently covered by JP's 'Pixel' are supported. diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 87b7302177..d5cd7e9039 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -30,14 +30,14 @@ import Data.Word (Word8) import Database.SQLite.Simple (SQLError (..)) import qualified Database.SQLite.Simple as DB import Foreign.C.String -import Foreign.C.Types (CInt (..)) +import Foreign.C.Types (CInt (..), CLong (..)) import Foreign.Ptr import Foreign.StablePtr import Foreign.Storable (poke) import GHC.IO.Encoding (setFileSystemEncoding, setForeignEncoding, setLocaleEncoding) import Simplex.Chat import Simplex.Chat.Controller -import Simplex.Chat.Image (readResizeable, resizeImageToStrSize) +import Simplex.Chat.Image (readResizeable, resizeImageToSize) import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList) import Simplex.Chat.Mobile.File import Simplex.Chat.Mobile.Shared @@ -105,6 +105,8 @@ foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Wo foreign export ccall "chat_write_file" cChatWriteFile :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CJSONString +foreign export ccall "chat_write_image" cChatWriteImage :: StablePtr ChatController -> CLong -> CString -> Ptr Word8 -> CInt -> IO CJSONString + foreign export ccall "chat_read_file" cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8) foreign export ccall "chat_encrypt_file" cChatEncryptFile :: StablePtr ChatController -> CString -> CString -> IO CJSONString @@ -208,7 +210,7 @@ cChatResizeImageToStrSize fp' maxSize = do fp <- peekCString fp' res <- runExceptT $ do (ri, _) <- liftIOEither $ readResizeable fp - let resized = resizeImageToStrSize (fromIntegral maxSize) ri + let resized = resizeImageToSize True (fromIntegral maxSize) ri if LB.length resized > fromIntegral maxSize then throwError "unable to fit" else pure resized newCStringFromLazyBS $ fromRight "" res diff --git a/src/Simplex/Chat/Mobile/File.hs b/src/Simplex/Chat/Mobile/File.hs index afbb1bc8c9..e429e9b9c7 100644 --- a/src/Simplex/Chat/Mobile/File.hs +++ b/src/Simplex/Chat/Mobile/File.hs @@ -7,6 +7,7 @@ module Simplex.Chat.Mobile.File ( cChatWriteFile, + cChatWriteImage, cChatReadFile, cChatEncryptFile, cChatDecryptFile, @@ -35,6 +36,8 @@ import Foreign.Ptr import Foreign.StablePtr import Foreign.Storable (poke, pokeByteOff) import Simplex.Chat.Controller (ChatController (..)) +import Simplex.Chat.Image (resizeImageToSize) +import qualified Simplex.Chat.Image as Picture import Simplex.Chat.Mobile.Shared import Simplex.Chat.Util (chunkSize, encryptFile) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..), CryptoFileHandle, FTCryptoError (..)) @@ -58,6 +61,21 @@ cChatWriteFile cc cPath ptr len = do r <- chatWriteFile c path s newCStringFromLazyBS $ J.encode r +cChatWriteImage :: StablePtr ChatController -> CLong -> CString -> Ptr Word8 -> CInt -> IO CJSONString +cChatWriteImage cc maxSize cPath ptr len = do + c <- deRefStablePtr cc + path <- peekCString cPath + src <- getByteString ptr len + r <- + case Picture.decodeResizeable src of + Left e -> pure $ WFError e + Right (ri, _metadata) -> do + let resized = resizeImageToSize True (fromIntegral maxSize) ri + if LB.length resized > fromIntegral maxSize + then pure $ WFError "unable to fit" + else chatWriteFile c path (LB.toStrict resized) + newCStringFromLazyBS $ J.encode r + chatWriteFile :: ChatController -> FilePath -> ByteString -> IO WriteFileResult chatWriteFile ChatController {random} path s = do cfArgs <- atomically $ CF.randomArgs random