Преглед на файлове

Simplify Image structure, close #244

Him188 преди 5 години
родител
ревизия
5b2ae6e9ad

+ 4 - 21
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/FriendImpl.kt

@@ -91,24 +91,14 @@ internal class FriendImpl(
                     fileId = 0,
                     fileMd5 = image.md5,
                     fileSize = image.inputSize.toInt(),
-                    fileName = image.md5.toUHexString("") + "." + image.format,
-                    imgOriginal = 1,
-                    imgWidth = image.width,
-                    imgHeight = image.height,
-                    imgType = image.imageType
+                    fileName = image.md5.toUHexString("") + ".gif",
+                    imgOriginal = 1
                 )
             ).sendAndExpect<LongConn.OffPicUp.Response>()
 
             @Suppress("UNCHECKED_CAST") // bug
             return when (response) {
-                is LongConn.OffPicUp.Response.FileExists -> OfflineFriendImage(
-                    filepath = response.resourceId,
-                    md5 = response.imageInfo.fileMd5,
-                    fileLength = response.imageInfo.fileSize.toInt(),
-                    height = response.imageInfo.fileHeight,
-                    width = response.imageInfo.fileWidth,
-                    resourceId = response.resourceId
-                ).also {
+                is LongConn.OffPicUp.Response.FileExists -> OfflineFriendImage(response.resourceId).also {
                     ImageUploadEvent.Succeed(this@FriendImpl, image, it).broadcast()
                 }
                 is LongConn.OffPicUp.Response.RequireUpload -> {
@@ -132,14 +122,7 @@ internal class FriendImpl(
                     //)
                     // 为什么不能 ??
 
-                    return OfflineFriendImage(
-                        filepath = response.resourceId,
-                        md5 = image.md5,
-                        fileLength = image.inputSize.toInt(),
-                        height = image.height,
-                        width = image.width,
-                        resourceId = response.resourceId
-                    ).also {
+                    return OfflineFriendImage(response.resourceId).also {
                         ImageUploadEvent.Succeed(this@FriendImpl, image, it).broadcast()
                     }
                 }

+ 5 - 13
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt

@@ -400,11 +400,7 @@ internal class GroupImpl(
                 uin = bot.id,
                 groupCode = id,
                 md5 = image.md5,
-                size = image.inputSize,
-                picWidth = image.width,
-                picHeight = image.height,
-                picType = image.imageType,
-                filename = image.filename
+                size = image.inputSize
             ).sendAndExpect()
 
             @Suppress("UNCHECKED_CAST") // bug
@@ -427,10 +423,8 @@ internal class GroupImpl(
 //                        fileId = response.fileId.toInt()
 //                    )
                     //  println("NMSL")
-                    return OfflineGroupImage(
-                        md5 = image.md5,
-                        filepath = resourceId
-                    ).also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
+                    return OfflineGroupImage(imageId = resourceId)
+                        .also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
                 }
                 is ImgStore.GroupPicUp.Response.RequireUpload -> {
                     // 每 10KB 等 1 秒, 最少等待 5 秒
@@ -480,10 +474,8 @@ internal class GroupImpl(
                     //     imageType = image.imageType,
                     //     fileId = response.fileId.toInt()
                     // )
-                    return OfflineGroupImage(
-                        md5 = image.md5,
-                        filepath = resourceId
-                    ).also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
+                    return OfflineGroupImage(imageId = resourceId)
+                        .also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
                     /*
                         fileId = response.fileId.toInt(),
                         fileType = 0, // ?

+ 4 - 4
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/FlashImageImpl.kt

@@ -2,6 +2,7 @@ package net.mamoe.mirai.qqandroid.message
 
 import net.mamoe.mirai.message.data.FriendFlashImage
 import net.mamoe.mirai.message.data.GroupFlashImage
+import net.mamoe.mirai.message.data.md5
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.HummerCommelem
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
 import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
@@ -13,7 +14,7 @@ internal fun GroupFlashImage.toJceData() = ImMsgBody.Elem(
         businessType = 0,
         pbElem = HummerCommelem.MsgElemInfoServtype3(
             flashTroopPic = ImMsgBody.CustomFace(
-                filePath = image.filepath,
+                filePath = image.imageId,
                 md5 = image.md5,
                 pbReserve = byteArrayOf(0x78, 0x06)
             )
@@ -27,9 +28,8 @@ internal fun FriendFlashImage.toJceData() = ImMsgBody.Elem(
         businessType = 0,
         pbElem = HummerCommelem.MsgElemInfoServtype3(
             flashC2cPic = ImMsgBody.NotOnlineImage(
-                filePath = image.filepath,
-                fileId = image.fileId,
-                resId = image.resourceId,
+                filePath = image.imageId,
+                resId = image.imageId,
                 picMd5 = image.md5,
                 oldPicMd5 = false,
                 pbReserve = byteArrayOf(0x78, 0x06)

+ 10 - 59
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/imagesImpl.kt

@@ -9,10 +9,7 @@
 
 package net.mamoe.mirai.qqandroid.message
 
-import net.mamoe.mirai.message.data.OfflineFriendImage
-import net.mamoe.mirai.message.data.OfflineGroupImage
-import net.mamoe.mirai.message.data.OnlineFriendImage
-import net.mamoe.mirai.message.data.OnlineGroupImage
+import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
 import net.mamoe.mirai.qqandroid.utils.hexToBytes
 import net.mamoe.mirai.utils.ExternalImage
@@ -20,28 +17,12 @@ import net.mamoe.mirai.utils.ExternalImage
 internal class OnlineGroupImageImpl(
     internal val delegate: ImMsgBody.CustomFace
 ) : OnlineGroupImage() {
-    override val filepath: String = delegate.filePath
-    override val fileId: Int get() = delegate.fileId
-    override val serverIp: Int get() = delegate.serverIp
-    override val serverPort: Int get() = delegate.serverPort
-    override val fileType: Int get() = delegate.fileType
-    override val signature: ByteArray get() = delegate.signature
-    override val useful: Int get() = delegate.useful
-    override val md5: ByteArray get() = delegate.md5
-    override val bizType: Int get() = delegate.bizType
-    override val imageType: Int get() = delegate.imageType
-    override val width: Int get() = delegate.width
-    override val height: Int get() = delegate.height
-    override val source: Int get() = delegate.source
-    override val size: Int get() = delegate.size
-    override val original: Int get() = delegate.origin
-    override val pbReserve: ByteArray get() = delegate.pbReserve
-    override val imageId: String = ExternalImage.generateImageId(delegate.md5, imageType)
+    override val imageId: String = ExternalImage.generateImageId(delegate.md5)
     override val originUrl: String
         get() = "http://gchat.qpic.cn" + delegate.origUrl
 
     override fun equals(other: Any?): Boolean {
-        return other is OnlineGroupImageImpl && other.filepath == this.filepath && other.md5.contentEquals(this.md5)
+        return other is OnlineGroupImageImpl && other.imageId == this.imageId
     }
 
     override fun hashCode(): Int {
@@ -52,23 +33,13 @@ internal class OnlineGroupImageImpl(
 internal class OnlineFriendImageImpl(
     internal val delegate: ImMsgBody.NotOnlineImage
 ) : OnlineFriendImage() {
-    override val resourceId: String get() = delegate.resId
-    override val md5: ByteArray get() = delegate.picMd5
-    override val filepath: String get() = delegate.filePath
-    override val fileLength: Int get() = delegate.fileLen
-    override val height: Int get() = delegate.picHeight
-    override val width: Int get() = delegate.picWidth
-    override val bizType: Int get() = delegate.bizType
-    override val imageType: Int get() = delegate.imgType
-    override val downloadPath: String get() = delegate.downloadPath
-    override val fileId: Int get() = delegate.fileId
+    override val imageId: String get() = delegate.resId
     override val original: Int get() = delegate.original
     override val originUrl: String
         get() = "http://c2cpicdw.qpic.cn" + this.delegate.origUrl
 
     override fun equals(other: Any?): Boolean {
-        return other is OnlineFriendImageImpl && other.resourceId == this.resourceId && other.md5
-            .contentEquals(this.md5)
+        return other is OnlineFriendImageImpl && other.imageId == this.imageId
     }
 
     override fun hashCode(): Int {
@@ -78,22 +49,8 @@ internal class OnlineFriendImageImpl(
 
 internal fun OfflineGroupImage.toJceData(): ImMsgBody.CustomFace {
     return ImMsgBody.CustomFace(
-        filePath = this.filepath,
-        fileId = this.fileId,
-        serverIp = this.serverIp,
-        serverPort = this.serverPort,
-        fileType = this.fileType,
-        signature = this.signature,
-        useful = this.useful,
+        filePath = this.imageId,
         md5 = this.md5,
-        bizType = this.bizType,
-        imageType = this.imageType,
-        width = this.width,
-        height = this.height,
-        source = this.source,
-        size = this.size,
-        origin = this.original,
-        pbReserve = this.pbReserve,
         flag = ByteArray(4),
         //_400Height = 235,
         //_400Url = "/gchatpic_new/1040400290/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",
@@ -108,18 +65,12 @@ private val oldData: ByteArray =
 
 internal fun OfflineFriendImage.toJceData(): ImMsgBody.NotOnlineImage {
     return ImMsgBody.NotOnlineImage(
-        filePath = this.filepath,
-        resId = this.resourceId,
+        filePath = this.imageId,
+        resId = this.imageId,
         oldPicMd5 = false,
         picMd5 = this.md5,
-        fileLen = this.fileLength,
-        picHeight = this.height,
-        picWidth = this.width,
-        bizType = this.bizType,
-        imgType = this.imageType,
-        downloadPath = this.downloadPath,
-        original = this.original,
-        fileId = this.fileId,
+        downloadPath = this.imageId,
+        original = 1,
         pbReserve = byteArrayOf(0x78, 0x02)
     )
 }

+ 1 - 2
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt

@@ -28,7 +28,6 @@ import net.mamoe.mirai.qqandroid.utils.cryptor.ECDH
 import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
 import net.mamoe.mirai.utils.*
 import kotlin.random.Random
-import kotlin.random.nextInt
 
 internal val DeviceInfo.guid: ByteArray get() = generateGuid(androidId, macAddress)
 
@@ -43,7 +42,7 @@ private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray
 /**
  * 生成长度为 [length], 元素为随机 `0..255` 的 [ByteArray]
  */
-internal fun getRandomByteArray(length: Int): ByteArray = ByteArray(length) { Random.nextInt(0..255).toByte() }
+internal fun getRandomByteArray(length: Int): ByteArray = ByteArray(length) { Random.nextInt(0, 255).toByte() }
 
 internal object DefaultServerList : Set<Pair<String, Int>> by setOf(
     "42.81.169.46" to 8080,

+ 2 - 2
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Cmd0x352.kt

@@ -129,8 +129,8 @@ internal class Cmd0x352 : ProtoBuf {
         @ProtoId(11) @JvmField val retry: Int = 0,//default
         @ProtoId(12) @JvmField val buType: Int = 1,//1或96 不确定
         @ProtoId(13) @JvmField val imgOriginal: Int,//是否为原图
-        @ProtoId(14) @JvmField val imgWidth: Int,
-        @ProtoId(15) @JvmField val imgHeight: Int,
+        @ProtoId(14) @JvmField val imgWidth: Int = 0,
+        @ProtoId(15) @JvmField val imgHeight: Int = 0,
         /**
          * ImgType:
          *  JPG:    1000

+ 14 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImgStore.kt

@@ -19,6 +19,19 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
 import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
 import net.mamoe.mirai.qqandroid.utils.io.serialization.readProtoBuf
 import net.mamoe.mirai.qqandroid.utils.io.serialization.writeProtoBuf
+import kotlin.random.Random
+import kotlin.random.nextInt
+
+internal fun getRandomString(length: Int): String =
+    getRandomString(length, *defaultRanges)
+
+private val defaultRanges: Array<CharRange> = arrayOf('a'..'z', 'A'..'Z', '0'..'9')
+
+internal fun getRandomString(length: Int, charRange: CharRange): String =
+    String(CharArray(length) { charRange.random() })
+
+internal fun getRandomString(length: Int, vararg charRanges: CharRange): String =
+    String(CharArray(length) { charRanges[Random.Default.nextInt(0..charRanges.lastIndex)].random() })
 
 internal class ImgStore {
     object GroupPicUp : OutgoingPacketFactory<GroupPicUp.Response>("ImgStore.GroupPicUp") {
@@ -33,7 +46,7 @@ internal class ImgStore {
             picHeight: Int = 0, // not orthodox
             picType: Int = 1000,
             fileId: Long = 0,
-            filename: String,
+            filename: String = getRandomString(16) + ".gif", // make server happier
             srcTerm: Int = 5,
             platformType: Int = 9,
             buType: Int = 1,

+ 27 - 89
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt

@@ -15,12 +15,12 @@
 package net.mamoe.mirai.message.data
 
 import kotlinx.serialization.Serializable
-import kotlinx.serialization.Transient
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.BotImpl
 import net.mamoe.mirai.contact.Contact
 import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.utils.MiraiInternalAPI
+import net.mamoe.mirai.utils.SinceMirai
 import kotlin.js.JsName
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
@@ -65,7 +65,18 @@ expect interface Image : Message, MessageContent {
 @Suppress("FunctionName")
 @JsName("newImage")
 @JvmName("newImage")
-fun Image(imageId: String): Image = when {
+fun Image(imageId: String): OfflineImage = when {
+    imageId.startsWith('/') -> OfflineFriendImage(imageId) // /f8f1ab55-bf8e-4236-b55e-955848d7069f
+    imageId.length == 42 || imageId.startsWith('{') && imageId.endsWith('}') -> OfflineGroupImage(imageId) // {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png
+    else -> throw IllegalArgumentException("illegal imageId: $imageId. $ILLEGAL_IMAGE_ID_EXCEPTION_MESSAGE")
+}
+
+@JvmSynthetic
+@Deprecated("for binary compatibility", level = DeprecationLevel.HIDDEN)
+@Suppress("FunctionName")
+@JsName("newImage")
+@JvmName("newImage")
+fun Image2(imageId: String): Image = when {
     imageId.startsWith('/') -> OfflineFriendImage(imageId) // /f8f1ab55-bf8e-4236-b55e-955848d7069f
     imageId.length == 42 || imageId.startsWith('{') && imageId.endsWith('}') -> OfflineGroupImage(imageId) // {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png
     else -> throw IllegalArgumentException("illegal imageId: $imageId. $ILLEGAL_IMAGE_ID_EXCEPTION_MESSAGE")
@@ -100,8 +111,7 @@ sealed class AbstractImage : Image {
  */
 interface OnlineImage : Image {
     companion object Key : Message.Key<OnlineImage> {
-        override val typeName: String
-            get() = "OnlineImage"
+        override val typeName: String get() = "OnlineImage"
     }
 
     /**
@@ -113,6 +123,7 @@ interface OnlineImage : Image {
 /**
  * 查询原图下载链接.
  */
+@JvmSynthetic
 suspend fun Image.queryUrl(): String {
     @OptIn(MiraiInternalAPI::class)
     return when (this) {
@@ -135,8 +146,7 @@ suspend fun Image.queryUrl(): String {
  */
 interface OfflineImage : Image {
     companion object Key : Message.Key<OfflineImage> {
-        override val typeName: String
-            get() = "OfflineImage"
+        override val typeName: String get() = "OfflineImage"
     }
 }
 
@@ -163,26 +173,8 @@ suspend fun OfflineImage.queryUrl(): String {
 @OptIn(MiraiInternalAPI::class)
 sealed class GroupImage : AbstractImage() {
     companion object Key : Message.Key<GroupImage> {
-        override val typeName: String
-            get() = "GroupImage"
+        override val typeName: String get() = "GroupImage"
     }
-
-    abstract val filepath: String
-    abstract val fileId: Int
-    abstract val serverIp: Int
-    abstract val serverPort: Int
-    abstract val fileType: Int
-    abstract val signature: ByteArray
-    abstract val useful: Int
-    abstract val md5: ByteArray
-    abstract val bizType: Int
-    abstract val imageType: Int
-    abstract val width: Int
-    abstract val height: Int
-    abstract val source: Int
-    abstract val size: Int
-    abstract val pbReserve: ByteArray
-    abstract val original: Int
 }
 
 /**
@@ -190,35 +182,13 @@ sealed class GroupImage : AbstractImage() {
  */
 @Serializable
 data class OfflineGroupImage(
-    override val filepath: String, // {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png
-    override val md5: ByteArray
-) : GroupImage(), OfflineImage {
-    constructor(imageId: String) : this(filepath = imageId, md5 = calculateImageMd5ByImageId(imageId))
-
-    override val fileId: Int get() = 0
-    override val serverIp: Int get() = 0
-    override val serverPort: Int get() = 0
-    override val fileType: Int get() = 0 // 0
-    override val signature: ByteArray get() = EMPTY_BYTE_ARRAY
-    override val useful: Int get() = 1
-    override val bizType: Int get() = 0
-    override val imageType: Int get() = 0
-    override val width: Int get() = 0
-    override val height: Int get() = 0
-    override val source: Int get() = 200
-    override val size: Int get() = 0
-    override val original: Int get() = 1
-    override val pbReserve: ByteArray get() = EMPTY_BYTE_ARRAY
-    override val imageId: String get() = filepath
-
-    override fun hashCode(): Int {
-        return filepath.hashCode() + 31 * md5.hashCode()
-    }
+    override val imageId: String
+) : GroupImage(), OfflineImage
 
-    override fun equals(other: Any?): Boolean {
-        return other is OfflineGroupImage && other::class == this::class && other.md5.contentEquals(this.md5) && other.filepath == this.filepath
-    }
-}
+@get:JvmName("calculateImageMd5")
+@SinceMirai("0.39.0")
+val Image.md5: ByteArray
+    get() = calculateImageMd5ByImageId(imageId)
 
 /**
  * 接收消息时获取到的 [GroupImage]. 它可以直接获取下载链接 [originUrl]
@@ -239,51 +209,19 @@ abstract class OnlineGroupImage : GroupImage(), OnlineImage
 @OptIn(MiraiInternalAPI::class)
 sealed class FriendImage : AbstractImage() {
     companion object Key : Message.Key<FriendImage> {
-        override val typeName: String
-            get() = "FriendImage"
+        override val typeName: String get() = "FriendImage"
     }
 
-    abstract val resourceId: String
-    abstract val md5: ByteArray
-    abstract val filepath: String
-    abstract val fileLength: Int
-    abstract val height: Int
-    abstract val width: Int
-    open val bizType: Int get() = 0
-    open val imageType: Int get() = 1000
-    abstract val fileId: Int
-    open val downloadPath: String get() = resourceId
     open val original: Int get() = 1
-
-    override val imageId: String get() = resourceId
 }
 
 /**
  * 通过 [Group.uploadImage] 上传得到的 [GroupImage]. 它的链接需要查询 [Bot.queryImageUrl]
  */
+@Serializable
 data class OfflineFriendImage(
-    override val resourceId: String,
-    override val md5: ByteArray,
-    @Transient override val filepath: String = resourceId,
-    @Transient override val fileLength: Int = 0,
-    @Transient override val height: Int = 0,
-    @Transient override val width: Int = 0,
-    @Transient override val bizType: Int = 0,
-    @Transient override val imageType: Int = 1000,
-    @Transient override val downloadPath: String = resourceId,
-    @Transient override val fileId: Int = 0
-) : FriendImage(), OfflineImage {
-    constructor(imageId: String) : this(resourceId = imageId, md5 = calculateImageMd5ByImageId(imageId))
-
-    override fun hashCode(): Int {
-        return resourceId.hashCode() + 31 * md5.hashCode()
-    }
-
-    override fun equals(other: Any?): Boolean {
-        return other is OfflineFriendImage && other::class == this::class &&
-                other.md5.contentEquals(this.md5) && other.resourceId == this.resourceId
-    }
-}
+    override val imageId: String
+) : FriendImage(), OfflineImage
 
 /**
  * 接收消息时获取到的 [FriendImage]. 它可以直接获取下载链接 [originUrl]

+ 20 - 72
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt

@@ -23,6 +23,7 @@ import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.OfflineImage
 import net.mamoe.mirai.message.data.sendTo
+import net.mamoe.mirai.message.data.toLongUnsigned
 import kotlin.jvm.JvmSynthetic
 
 /**
@@ -34,52 +35,32 @@ import kotlin.jvm.JvmSynthetic
  * @See ExternalImage.upload 上传图片并得到 [Image] 消息
  */
 class ExternalImage private constructor(
-    val width: Int,
-    val height: Int,
     val md5: ByteArray,
-    imageFormat: String,
     val input: Any, // Input from kotlinx.io, InputStream from kotlinx.io MPP, ByteReadChannel from ktor
-    val inputSize: Long, // dont be greater than Int.MAX
-    val filename: String
+    val inputSize: Long // dont be greater than Int.MAX
 ) {
     constructor(
-        width: Int,
-        height: Int,
         md5: ByteArray,
-        imageFormat: String,
         input: ByteReadChannel,
-        inputSize: Long, // dont be greater than Int.MAX
-        filename: String
-    ) : this(width, height, md5, imageFormat, input as Any, inputSize, filename)
+        inputSize: Long // dont be greater than Int.MAX
+    ) : this(md5, input as Any, inputSize)
 
     constructor(
-        width: Int,
-        height: Int,
         md5: ByteArray,
-        imageFormat: String,
         input: Input,
-        inputSize: Long, // dont be greater than Int.MAX
-        filename: String
-    ) : this(width, height, md5, imageFormat, input as Any, inputSize, filename)
+        inputSize: Long // dont be greater than Int.MAX
+    ) : this(md5, input as Any, inputSize)
 
     constructor(
-        width: Int,
-        height: Int,
         md5: ByteArray,
-        imageFormat: String,
-        input: ByteReadPacket,
-        filename: String
-    ) : this(width, height, md5, imageFormat, input as Any, input.remaining, filename)
+        input: ByteReadPacket
+    ) : this(md5, input as Any, input.remaining)
 
     @OptIn(InternalSerializationApi::class)
     constructor(
-        width: Int,
-        height: Int,
         md5: ByteArray,
-        imageFormat: String,
-        input: InputStream,
-        filename: String
-    ) : this(width, height, md5, imageFormat, input as Any, input.available().toLong(), filename)
+        input: InputStream
+    ) : this(md5, input as Any, input.available().toLongUnsigned())
 
     init {
         require(inputSize < 30L * 1024 * 1024) { "file is too big. Maximum is about 20MB" }
@@ -87,63 +68,30 @@ class ExternalImage private constructor(
 
     companion object {
         fun generateUUID(md5: ByteArray): String {
-            return "${md5[0..3]}-${md5[4..5]}-${md5[6..7]}-${md5[8..9]}-${md5[10..15]}"
+            return "${md5[0, 3]}-${md5[4, 5]}-${md5[6, 7]}-${md5[8, 9]}-${md5[10, 15]}"
         }
 
-        fun generateImageId(md5: ByteArray, imageType: Int): String {
-            return """{${generateUUID(md5)}}.${determineFormat(imageType)}"""
-        }
-
-        fun determineImageType(format: String): Int {
-            return when (format) {
-                "jpg" -> 1000
-                "png" -> 1001
-                "webp" -> 1002
-                "bmp" -> 1005
-                "gig" -> 2000
-                "apng" -> 2001
-                "sharpp" -> 1004
-                else -> 1000 // unsupported, just make it jpg
-            }
-        }
-
-        fun determineFormat(imageType: Int): String {
-            return when (imageType) {
-                1000 -> "jpg"
-                1001 -> "png"
-                1002 -> "webp"
-                1005 -> "bmp"
-                2000 -> "gig"
-                2001 -> "apng"
-                1004 -> "sharpp"
-                else -> "jpg" // unsupported, just make it jpg
-            }
+        fun generateImageId(md5: ByteArray): String {
+            return """{${generateUUID(md5)}}.gif"""
         }
     }
 
-    val format: String =
-        when (val it = imageFormat.toLowerCase()) {
-            "jpeg" -> "jpg" //必须转换
-            else -> it
-        }
-
-    /**
+    /*
      * ImgType:
      *  JPG:    1000
      *  PNG:    1001
      *  WEBP:   1002
      *  BMP:    1005
-     *  GIG:    2000 // TODO gig? gif?
+     *  GIG:    2000 // gig? gif?
      *  APNG:   2001
      *  SHARPP: 1004
      */
-    val imageType: Int
-        get() = determineImageType(format)
 
-    override fun toString(): String = "[ExternalImage(${width}x$height $format)]"
+
+    override fun toString(): String = "[ExternalImage(${generateUUID(md5)})]"
 
     fun calculateImageResourceId(): String {
-        return "{${generateUUID(md5)}}.$format"
+        return "{${generateUUID(md5)}}.gif"
     }
 }
 
@@ -176,8 +124,8 @@ suspend fun ExternalImage.upload(contact: Contact): OfflineImage = when (contact
 @JvmSynthetic
 suspend inline fun <C : Contact> C.sendImage(image: ExternalImage): MessageReceipt<C> = image.sendTo(this)
 
-internal operator fun ByteArray.get(range: IntRange): String = buildString {
-    range.forEach {
+internal operator fun ByteArray.get(rangeStart: Int, rangeEnd: Int): String = buildString {
+    for (it in rangeStart..rangeEnd) {
         append(this@get[it].fixToString())
     }
 }

+ 3 - 3
mirai-core/src/commonTest/kotlin/net/mamoe/mirai/utils/ExternalImageTest.kt

@@ -7,8 +7,8 @@ internal class ExternalImageTest {
 
     @Test
     fun testByteArrayGet() {
-        assertEquals("0F", byteArrayOf(0x0f)[0..0])
-        assertEquals("10", byteArrayOf(0x10)[0..0])
-        assertEquals("0FFE", byteArrayOf(0x0F, 0xFE.toByte())[0..1])
+        assertEquals("0F", byteArrayOf(0x0f)[0, 0])
+        assertEquals("10", byteArrayOf(0x10)[0, 0])
+        assertEquals("0FFE", byteArrayOf(0x0F, 0xFE.toByte())[0, 1])
     }
 }

+ 1 - 2
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/data/Image.kt

@@ -43,8 +43,7 @@ import java.net.URL
  */
 actual interface Image : Message, MessageContent {
     actual companion object Key : Message.Key<Image> {
-        actual override val typeName: String
-            get() = "Image"
+        actual override val typeName: String get() = "Image"
     }
 
     /**

+ 19 - 20
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt

@@ -15,7 +15,6 @@ import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.io.ByteReadChannel
 import kotlinx.coroutines.withContext
 import kotlinx.io.core.Input
-import kotlinx.io.core.buildPacket
 import kotlinx.io.core.copyTo
 import kotlinx.io.errors.IOException
 import kotlinx.io.streams.asOutput
@@ -34,49 +33,49 @@ import javax.imageio.ImageIO
 
 
 /**
- * 读取 [BufferedImage] 的属性, 然后构造 [ExternalImage]
+ * 将 [BufferedImage] 保存稳临时文件, 然后构造 [ExternalImage]
  */
 @JvmOverloads
 @Throws(IOException::class)
 fun BufferedImage.toExternalImage(formatName: String = "gif"): ExternalImage {
+    val file = createTempFile().apply { deleteOnExit() }
+
     val digest = MessageDigest.getInstance("md5")
     digest.reset()
 
-    val buffer = buildPacket {
+    file.outputStream().use { out ->
         ImageIO.write(this@toExternalImage, formatName, object : OutputStream() {
             override fun write(b: Int) {
-                b.toByte().let {
-                    [email protected](it)
-                    digest.update(it)
-                }
+                out.write(b)
+                digest.update(b.toByte())
+            }
+
+            override fun write(b: ByteArray) {
+                out.write(b)
+                digest.update(b)
+            }
+
+            override fun write(b: ByteArray, off: Int, len: Int) {
+                out.write(b, off, len)
+                digest.update(b, off, len)
             }
         })
     }
 
-    return ExternalImage(width, height, digest.digest(), formatName, buffer, getRandomString(16) + "." + formatName)
+    return ExternalImage(digest.digest(), file.inputStream())
 }
 
 suspend inline fun BufferedImage.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
 
 /**
- * 读取文件头识别图片属性, 然后构造 [ExternalImage]
+ * 直接使用文件 [inputStream] 构造 [ExternalImage]
  */
 @OptIn(MiraiInternalAPI::class)
 @Throws(IOException::class)
 fun File.toExternalImage(): ExternalImage {
-    val input = ImageIO.createImageInputStream(this)
-    checkNotNull(input) { "Unable to read file(path=${this.path}), no ImageInputStream found" }
-    val image = ImageIO.getImageReaders(input).asSequence().firstOrNull()
-        ?: error("Unable to read file(path=${this.path}), no ImageReader found (file type not supported)")
-    image.input = input
-
     return ExternalImage(
-        width = image.getWidth(0),
-        height = image.getHeight(0),
         md5 = this.inputStream().md5(), // dont change
-        imageFormat = image.formatName,
-        input = this.inputStream(),
-        filename = this.name
+        input = this.inputStream()
     )
 }
 

+ 3 - 1
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/SystemDeviceInfo.kt

@@ -87,7 +87,9 @@ internal fun getRandomByteArray(length: Int): ByteArray = ByteArray(length) { Ra
  * 随机生成长度为 [length] 的 [String].
  */
 internal fun getRandomString(length: Int): String =
-    getRandomString(length, 'a'..'z', 'A'..'Z', '0'..'9')
+    getRandomString(length, *defaultRanges)
+
+private val defaultRanges: Array<CharRange> = arrayOf('a'..'z', 'A'..'Z', '0'..'9')
 
 /**
  * 根据所给 [charRange] 随机生成长度为 [length] 的 [String].