Simplify Image structure, close #244

This commit is contained in:
Him188 2020-04-23 17:10:21 +08:00
parent eb123bb3a9
commit 5b2ae6e9ad
13 changed files with 113 additions and 289 deletions

View File

@ -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()
}
}

View File

@ -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, // ?

View File

@ -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)

View File

@ -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)
)
}

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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 imageId: String
) : GroupImage(), OfflineImage
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 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]

View File

@ -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())
}
}

View File

@ -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])
}
}

View File

@ -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"
}
/**

View File

@ -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 {
this@buildPacket.writeByte(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()
)
}

View File

@ -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].