mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 06:32:59 +00:00
ios, android, desktop: load JPEG, PNG, GIF and WebP image files (#7956)
This commit is contained in:
1 parent
1fd7ed8327
commit
5230eadbab
7 files changed
+94
-7
No files matched your search
@@ -112,7 +112,7 @@ private func imagesWithinBudget() -> Modifier {
|
||||
guard parts.count == 2,
|
||||
parts[0].hasSuffix(";base64"),
|
||||
let data = Data(base64Encoded: String(parts[1])),
|
||||
data.starts(with: [0xFF, 0xD8, 0xFF]) || data.starts(with: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
|
||||
ImageFileType.jpeg.matches(data) || ImageFileType.png.matches(data),
|
||||
let size = UIImage(data: data)?.size,
|
||||
size.width <= MAX_MARKDOWN_IMAGE_DIMENSION,
|
||||
size.height <= MAX_MARKDOWN_IMAGE_DIMENSION,
|
||||
|
||||
@@ -24,6 +24,7 @@ public func getLoadedImage(_ file: CIFile?) -> UIImage? {
|
||||
let filePath = getAppFilePath(fileSource.filePath)
|
||||
do {
|
||||
let data = try getFileData(filePath, fileSource.cryptoArgs)
|
||||
guard isImageFile(data) else { return nil }
|
||||
let img = UIImage(data: data)
|
||||
do {
|
||||
try img?.setGifFromData(data, levelOfIntegrity: 1.0)
|
||||
@@ -38,6 +39,36 @@ public func getLoadedImage(_ file: CIFile?) -> UIImage? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public enum ImageFileType: CaseIterable {
|
||||
case jpeg
|
||||
case png
|
||||
case gif
|
||||
case webp
|
||||
|
||||
var signatures: [[UInt8?]] {
|
||||
switch self {
|
||||
case .jpeg: [[0xFF, 0xD8, 0xFF]]
|
||||
case .png: [[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]]
|
||||
case .gif: [ascii("GIF87a"), ascii("GIF89a")]
|
||||
case .webp: [ascii("RIFF") + [nil, nil, nil, nil] + ascii("WEBP")]
|
||||
}
|
||||
}
|
||||
|
||||
public func matches(_ data: Data) -> Bool {
|
||||
signatures.contains { signature in
|
||||
data.count >= signature.count && zip(signature, data).allSatisfy { b, d in b == nil || b == d }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func isImageFile(_ data: Data) -> Bool {
|
||||
ImageFileType.allCases.contains { $0.matches(data) }
|
||||
}
|
||||
|
||||
private func ascii(_ s: String) -> [UInt8?] {
|
||||
s.utf8.map { $0 }
|
||||
}
|
||||
|
||||
public func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data {
|
||||
if let cfArgs = cfArgs {
|
||||
return try readCryptoFile(path: path.path, cryptoArgs: cfArgs)
|
||||
|
||||
+1
@@ -183,6 +183,7 @@ actual suspend fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>?
|
||||
} else {
|
||||
File(getAppFilePath(file.fileName)).readBytes()
|
||||
}
|
||||
if (!isImageFile(data)) return null
|
||||
decodeSampledBitmapFromByteArray(data, 1000, 1000).asImageBitmap() to data
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
|
||||
+15
@@ -29,6 +29,21 @@ expect fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap
|
||||
expect fun isImage(uri: URI): Boolean
|
||||
expect fun isAnimImage(uri: URI, drawable: Any?): Boolean
|
||||
|
||||
enum class ImageFileType(val signatures: List<List<Int?>>) {
|
||||
JPEG(listOf(listOf(0xFF, 0xD8, 0xFF))),
|
||||
PNG(listOf(listOf(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A))),
|
||||
GIF(listOf(ascii("GIF87a"), ascii("GIF89a"))),
|
||||
WEBP(listOf(ascii("RIFF") + List(4) { null } + ascii("WEBP")));
|
||||
|
||||
fun matches(data: ByteArray): Boolean = signatures.any { signature ->
|
||||
data.size >= signature.size && signature.withIndex().all { (i, b) -> b == null || b == data[i].toInt() and 0xFF }
|
||||
}
|
||||
}
|
||||
|
||||
fun isImageFile(data: ByteArray): Boolean = ImageFileType.entries.any { it.matches(data) }
|
||||
|
||||
private fun ascii(s: String): List<Int?> = s.map { it.code }
|
||||
|
||||
expect fun loadImageBitmap(inputStream: InputStream): ImageBitmap
|
||||
|
||||
expect fun resizeImageFileUri(uri: URI, maxSize: Int): String
|
||||
|
||||
+2
-6
@@ -54,10 +54,7 @@ import java.nio.charset.CodingErrorAction
|
||||
import java.util.Base64
|
||||
|
||||
private const val DATA_IMAGE = "data:image/"
|
||||
private val IMAGE_SIGNATURES = listOf(
|
||||
listOf(0xFF, 0xD8, 0xFF),
|
||||
listOf(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A)
|
||||
)
|
||||
private val IMAGE_TYPES = listOf(ImageFileType.JPEG, ImageFileType.PNG)
|
||||
private const val MAX_MARKDOWN_VIEW_SIZE: Long = 1_048_576
|
||||
private const val MAX_MARKDOWN_LINE_LENGTH = 15_000
|
||||
private const val MAX_MARKDOWN_DEPTH = 32
|
||||
@@ -295,6 +292,5 @@ private class DataImageTransformer : ImageTransformer {
|
||||
|
||||
private fun hasImageSignature(base64: String): Boolean {
|
||||
val header = runCatching { Base64.getDecoder().decode(base64.take(12)) }.getOrNull() ?: return false
|
||||
val bytes = header.map { it.toInt() and 0xFF }
|
||||
return IMAGE_SIGNATURES.any { bytes.take(it.size) == it }
|
||||
return IMAGE_TYPES.any { it.matches(header) }
|
||||
}
|
||||
+1
@@ -142,6 +142,7 @@ actual suspend fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>?
|
||||
return if (filePath != null) {
|
||||
loadedImageCache[filePath] ?: try {
|
||||
val data = if (file?.fileSource?.cryptoArgs != null) readCryptoFile(filePath, file.fileSource.cryptoArgs) else File(filePath).readBytes()
|
||||
if (!isImageFile(data)) return null
|
||||
val bitmap = decodeBoundedImage(data, MAX_THUMBNAIL_DIMENSION)
|
||||
(bitmap to data).also { loadedImageCache[filePath] = it }
|
||||
} catch (e: Exception) {
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import chat.simplex.common.platform.ImageFileType
|
||||
import chat.simplex.common.platform.isImageFile
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayOutputStream
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ImageFileTypeTest {
|
||||
@Test
|
||||
fun testJpegImagesLoad() {
|
||||
assertTrue(ImageFileType.JPEG.matches(encoded("jpg")))
|
||||
assertTrue(isImageFile(encoded("jpg")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPngImagesLoad() {
|
||||
assertTrue(ImageFileType.PNG.matches(encoded("png")))
|
||||
assertTrue(isImageFile(encoded("png")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGifImagesLoad() {
|
||||
assertTrue(ImageFileType.GIF.matches(encoded("gif")))
|
||||
assertTrue(isImageFile(encoded("gif")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWebpImagesLoad() {
|
||||
assertTrue(ImageFileType.WEBP.matches(webpHeader))
|
||||
assertTrue(isImageFile(webpHeader))
|
||||
}
|
||||
|
||||
private fun encoded(format: String): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
ImageIO.write(BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB), format, out)
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
private val webpHeader = listOf(0x52, 0x49, 0x46, 0x46, 0x36, 0xC8, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58).map { it.toByte() }.toByteArray()
|
||||
}
|
||||
Reference in new issue
Block a user