add chatWriteImage

This commit is contained in:
IC Rainbow
2024-11-01 23:06:28 +02:00
parent 7fee8b0dd5
commit 7fbf37e523
13 changed files with 123 additions and 91 deletions

View File

@@ -25,6 +25,18 @@ public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs {
}
}
public func writeCryptoImage(maxSize: Int, path: String, data: Data, encrypted: Bool) throws -> CryptoFileArgs {
let ptr: UnsafeMutableRawPointer = malloc(data.count)
memcpy(ptr, (data as NSData).bytes, data.count)
var cPath = path.cString(using: .utf8)!
let cjson = chat_write_image(getChatCtrl(), maxSize, &cPath, ptr, Int32(data.count), encrypted)!
let d = fromCString(cjson).data(using: .utf8)!
switch try jsonDecoder.decode(WriteFileResult.self, from: d) {
case let .result(cfArgs): return cfArgs
case let .error(err): throw RuntimeError(err)
}
}
public func readCryptoFile(path: String, cryptoArgs: CryptoFileArgs) throws -> Data {
var cPath = path.cString(using: .utf8)!
var cKey = cryptoArgs.fileKey.cString(using: .utf8)!

View File

@@ -223,6 +223,17 @@ public func saveFile(_ data: Data, _ fileName: String, encrypted: Bool) -> Crypt
}
}
public func saveImage(_ data: Data, _ fileName: String, maxSize: Long, encrypted: Bool) -> CryptoFile? {
let filePath = getAppFilePath(fileName)
do {
let cfArgs = try writeCryptoImage(maxSize: maxSize, path: filePath.path, data: data, encrypted: encrypted)
return CryptoFile(filePath: fileName, cryptoArgs: cfArgs)
} catch {
logger.error("FileUtils.saveImage error: \(error.localizedDescription)")
return nil
}
}
public func removeFile(_ url: URL) {
do {
try FileManager.default.removeItem(atPath: url.path)

View File

@@ -35,7 +35,7 @@ extern char *chat_resize_image_to_str_size(const char *path, long maxSize);
extern char *chat_write_file(chat_ctrl ctl, char *path, char *data, int len);
// chat_write_image returns null-terminated string with JSON of WriteFileResult
extern char *chat_write_image(chat_ctrl ctl, long maxSize, char *path, char *data, int len);
extern char *chat_write_image(chat_ctrl ctl, long maxSize, char *path, char *data, int len, bool encrypted);
// chat_read_file returns a buffer with:
// result status (1 byte), then if

View File

@@ -72,11 +72,11 @@ fun Bitmap.clipToCircle(): Bitmap {
return circle
}
actual fun compressImageStr(bitmap: ImageBitmap): String {
val usePng = bitmap.hasAlpha()
val ext = if (usePng) "png" else "jpg"
return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP)
}
// actual fun compressImageStr(bitmap: ImageBitmap): String {
// val usePng = bitmap.hasAlpha()
// val ext = if (usePng) "png" else "jpg"
// return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP)
// }
actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream {
val stream = ByteArrayOutputStream()
@@ -84,19 +84,19 @@ actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOut
return stream
}
actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
var img = image
var stream = compressImageData(img, usePng)
while (stream.size() > maxDataSize) {
val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
val clippedRatio = min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt()
val height = img.height * width / img.width
img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap()
stream = compressImageData(img, usePng)
}
return stream
}
// actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
// var img = image
// var stream = compressImageData(img, usePng)
// while (stream.size() > maxDataSize) {
// val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
// val clippedRatio = min(ratio, 2.0)
// val width = (img.width.toDouble() / clippedRatio).toInt()
// val height = img.height * width / img.width
// img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap()
// stream = compressImageData(img, usePng)
// }
// return stream
// }
actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBitmap.grayToBitmap(this, Bitmap.Config.RGB_565).asImageBitmap()

View File

@@ -1,5 +1,6 @@
#include <jni.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
//#include <stdlib.h>
//#include <android/log.h>
@@ -68,7 +69,7 @@ extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_write_image(chat_ctrl ctl, long maxSize, char *path, char *data, int len);
extern char *chat_write_image(chat_ctrl ctrl, long max_size, const char *path, char *ptr, int length, bool encrypt);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path);
@@ -185,11 +186,11 @@ Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteImage(JNIEnv *env, jclass clazz, jlong controller, jlong maxSize, jstring path, jobject buffer) {
Java_chat_simplex_common_platform_CoreKt_chatWriteImage(JNIEnv *env, jclass clazz, jlong controller, jlong maxSize, jstring path, jobject buffer, jboolean encrypt) {
const char *_path = encode_to_utf8_chars(env, path);
jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer);
jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer);
jstring res = decode_to_utf8_string(env, chat_write_image((void*)controller, maxSize, _path, buff, capacity));
jstring res = decode_to_utf8_string(env, chat_write_image((void*)controller, maxSize, _path, buff, capacity, encrypt));
(*env)->ReleaseStringUTFChars(env, path, _path);
return res;
}

View File

@@ -1,5 +1,6 @@
#include <jni.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdint.h>
@@ -41,7 +42,7 @@ extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_write_image(chat_ctrl ctrl, long max_size, const char *path, char *ptr, int length);
extern char *chat_write_image(chat_ctrl ctrl, long max_size, const char *path, char *ptr, int length, bool encrypt);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path);
@@ -195,11 +196,11 @@ Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteImage(JNIEnv *env, jclass clazz, jlong controller, jlong maxSize, jstring path, jobject buffer) {
Java_chat_simplex_common_platform_CoreKt_chatWriteImage(JNIEnv *env, jclass clazz, jlong controller, jlong maxSize, jstring path, jobject buffer, jboolean encrypt) {
const char *_path = encode_to_utf8_chars(env, path);
jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer);
jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer);
jstring res = decode_to_utf8_string(env, chat_write_image((void*)controller, maxSize, _path, buff, capacity));
jstring res = decode_to_utf8_string(env, chat_write_image((void*)controller, maxSize, _path, buff, capacity, encrypt));
(*env)->ReleaseStringUTFChars(env, path, _path);
return res;
}

View File

@@ -1,5 +1,6 @@
package chat.simplex.common.model
import androidx.compose.ui.graphics.ImageBitmap
import chat.simplex.common.platform.*
import kotlinx.serialization.*
import java.nio.ByteBuffer
@@ -32,6 +33,19 @@ fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs {
}
}
fun writeCryptoImage(maxSize: Long, image: ImageBitmap, path: String, encrypt: Boolean): CryptoFileArgs {
val ctrl = ChatController.ctrl ?: throw Exception("Controller is not initialized")
val data = compressImageData(image, image.hasAlpha).toByteArray()
val buffer = ByteBuffer.allocateDirect(data.size)
buffer.put(data)
buffer.rewind()
val str = chatWriteImage(ctrl, maxSize, path, buffer, encrypt)
return when (val d = json.decodeFromString(WriteFileResult.serializer(), str)) {
is WriteFileResult.Result -> d.cryptoArgs
is WriteFileResult.Error -> throw Exception(d.writeError)
}
}
fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray {
val res: Array<Any> = chatReadFile(path, cryptoArgs.fileKey, cryptoArgs.fileNonce)
val status = (res[0] as Integer).toInt()

View File

@@ -32,6 +32,7 @@ external fun chatPasswordHash(pwd: String, salt: String): String
external fun chatValidName(name: String): String
external fun chatJsonLength(str: String): Int
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
external fun chatWriteImage(ctrl: ChatCtrl, maxSize: Long, path: String, buffer: ByteBuffer, encrypt: Boolean): String
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String

View File

@@ -9,9 +9,9 @@ import java.net.URI
expect fun base64ToBitmap(base64ImageString: String): ImageBitmap
// XXX: Not a part of platform services anymore?
expect fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String
expect fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream
// expect fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream
expect fun cropToSquare(image: ImageBitmap): ImageBitmap
expect fun compressImageStr(bitmap: ImageBitmap): String
// expect fun compressImageStr(bitmap: ImageBitmap): String
expect fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream
expect fun GrayU8.toImageBitmap(): ImageBitmap

View File

@@ -169,24 +169,19 @@ fun saveImage(image: ImageBitmap): CryptoFile? {
return try {
val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()
val ext = if (image.hasAlpha()) "png" else "jpg"
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
val destFileName = generateNewFileName("IMG", ext, File(getAppFilePath("")))
val destFile = File(getAppFilePath(destFileName))
if (encrypted) {
try {
val args = writeCryptoFile(destFile.absolutePath, dataResized.toByteArray())
try {
val args = writeCryptoImage(MAX_IMAGE_SIZE, image, destFile.absolutePath, encrypted)
if (encrypted) {
CryptoFile(destFileName, args)
} catch (e: Exception) {
Log.e(TAG, "Unable to write crypto file: " + e.stackTraceToString())
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
null
} else {
CryptoFile.plain(destFileName)
}
} else {
val output = FileOutputStream(destFile)
dataResized.writeTo(output)
output.flush()
output.close()
CryptoFile.plain(destFileName)
} catch (e: Exception) {
Log.e(TAG, "Unable to write crypto file: " + e.stackTraceToString())
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
null
}
} catch (e: Exception) {
Log.e(TAG, "Util.kt saveImage error: ${e.stackTraceToString()}")
@@ -198,14 +193,10 @@ fun desktopSaveImageInTmp(uri: URI): CryptoFile? {
val image = getBitmapFromUri(uri) ?: return null
return try {
val ext = if (image.hasAlpha()) "png" else "jpg"
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
val destFileName = generateNewFileName("IMG", ext, tmpDir)
val destFile = File(tmpDir, destFileName)
val output = FileOutputStream(destFile)
dataResized.writeTo(output)
output.flush()
output.close()
CryptoFile.plain(destFile.absolutePath)
val args = writeCryptoImage(MAX_IMAGE_SIZE, image, destFile.absolutePath, false)
CryptoFile(destFileName, args)
} catch (e: Exception) {
Log.e(TAG, "Util.kt desktopSaveImageInTmp error: ${e.stackTraceToString()}")
null
@@ -301,11 +292,7 @@ fun saveWallpaperFile(uri: URI): String? {
fun saveWallpaperFile(image: ImageBitmap): String {
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
val destFile = File(getWallpaperFilePath(destFileName))
val dataResized = resizeImageToDataSize(image, false, maxDataSize = 5_000_000)
val output = FileOutputStream(destFile)
dataResized.use {
it.writeTo(output)
}
writeCryptoImage(5_000_000, image, destFile.absolutePath, false)
return destFile.name
}

View File

@@ -46,19 +46,19 @@ actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String {
return str
}
actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
var img = image
var stream = compressImageData(img, usePng)
while (stream.size() > maxDataSize) {
val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
val clippedRatio = kotlin.math.min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt()
val height = img.height * width / img.width
img = img.scale(width, height)
stream = compressImageData(img, usePng)
}
return stream
}
// actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
// var img = image
// var stream = compressImageData(img, usePng)
// while (stream.size() > maxDataSize) {
// val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
// val clippedRatio = kotlin.math.min(ratio, 2.0)
// val width = (img.width.toDouble() / clippedRatio).toInt()
// val height = img.height * width / img.width
// img = img.scale(width, height)
// stream = compressImageData(img, usePng)
// }
// return stream
// }
actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
var xOffset = 0
@@ -73,17 +73,17 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
return image
}
actual fun compressImageStr(bitmap: ImageBitmap): String {
val usePng = bitmap.hasAlpha()
val ext = if (usePng) "png" else "jpg"
return try {
val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray())
"data:image/$ext;base64,$encoded"
} catch (e: Exception) {
Log.e(TAG, "resizeImageToStrSize error: $e")
throw e
}
}
// actual fun compressImageStr(bitmap: ImageBitmap): String {
// val usePng = bitmap.hasAlpha()
// val ext = if (usePng) "png" else "jpg"
// return try {
// val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray())
// "data:image/$ext;base64,$encoded"
// } catch (e: Exception) {
// Log.e(TAG, "resizeImageToStrSize error: $e")
// throw e
// }
// }
actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream {
val writer = ImageIO.getImageWritersByFormatName(if (usePng) "png" else "jpg").next()

View File

@@ -30,7 +30,7 @@ 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 (..), CLong (..))
import Foreign.C.Types (CBool (..), CInt (..), CLong (..))
import Foreign.Ptr
import Foreign.StablePtr
import Foreign.Storable (poke)
@@ -105,7 +105,7 @@ 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_write_image" cChatWriteImage :: StablePtr ChatController -> CLong -> CString -> Ptr Word8 -> CInt -> CBool -> IO CJSONString
foreign export ccall "chat_read_file" cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8)

View File

@@ -48,7 +48,7 @@ import Simplex.Messaging.Util (catchAll)
import UnliftIO (Handle, IOMode (..), atomically, withFile)
data WriteFileResult
= WFResult {cryptoArgs :: CryptoFileArgs}
= WFResult {cryptoArgs :: Maybe CryptoFileArgs}
| WFError {writeError :: String}
$(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "WF") ''WriteFileResult)
@@ -61,28 +61,33 @@ 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
chatWriteFile :: ChatController -> FilePath -> ByteString -> IO WriteFileResult
chatWriteFile ChatController {random} path s = do
cfArgs <- atomically $ CF.randomArgs random
chatWriteFile_ (Just cfArgs) path s
chatWriteFile_ :: Maybe CryptoFileArgs -> FilePath -> ByteString -> IO WriteFileResult
chatWriteFile_ cfArgs_ path s = do
let file = CryptoFile path cfArgs_
either WFError (\_ -> WFResult cfArgs_)
<$> runCatchExceptT (withExceptT show $ CF.writeFile file $ LB.fromStrict s)
cChatWriteImage :: StablePtr ChatController -> CLong -> CString -> Ptr Word8 -> CInt -> CBool -> IO CJSONString
cChatWriteImage cc maxSize cPath ptr len encrypt = do
c <- deRefStablePtr cc
path <- peekCString cPath
src <- getByteString ptr len
cfArgs_ <- if encrypt /= 0 then Just <$> atomically (CF.randomArgs $ random c) else pure Nothing
r <-
case Picture.decodeResizeable src of
Left e -> pure $ WFError e
Right (ri, _metadata) -> do
let resized = resizeImageToSize True (fromIntegral maxSize) ri
let resized = resizeImageToSize False (fromIntegral maxSize) ri
if LB.length resized > fromIntegral maxSize
then pure $ WFError "unable to fit"
else chatWriteFile c path (LB.toStrict resized)
else chatWriteFile_ cfArgs_ 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
let file = CryptoFile path $ Just cfArgs
either WFError (\_ -> WFResult cfArgs)
<$> runCatchExceptT (withExceptT show $ CF.writeFile file $ LB.fromStrict s)
data ReadFileResult
= RFResult {fileSize :: Int}
| RFError {readError :: String}
@@ -124,7 +129,7 @@ chatEncryptFile ChatController {random} fromPath toPath =
encrypt = do
cfArgs <- atomically $ CF.randomArgs random
encryptFile fromPath toPath cfArgs
pure cfArgs
pure $ Just cfArgs
cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString
cChatDecryptFile cFromPath cKey cNonce cToPath = do