mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-27 17:00:14 +08:00
Add height, width, size and imageType attribute to Image class (#1548)
* Add height, width, size and imageType attribute to Image class Close #1543 #1204 #1032 * Let ImageType enum code before the `Internals`, Also add @JvmStatic and OrNull version for match method * Use runBIO and throws annotation for getImageInfo * Add .kt suffix for MPP imagesImpl * Return empty imageInfo for unsupported images * Fix wrong file name * apiDump * Renaming MPP file instead of adding JvmName annotation * Optimize readability and only use BIO at call-site * Fix bug for detecting image type * Detecting javax module for java 9+ * Clean up * Disable some image types which not supported * Use cross platform code to read images, readd support for apng * Fix bug in reading image * apiDump * Fix bug in image reading and write unite test * Fix wording * Remove webp support and throws IllegalArgumentException for unsupported format * Remove WEBP enum type * Add unit test for unsupported image and correct comments * Fix buffer input stream error when reading images * Applying suggestions and fix jpg reading * Add complete SOF decoding for jpg and clean up * Caching jpg sof byte ranges * Save `values()` as IMAGE_TYPE_ENUM_LIST for preventing clone operation * Remove duplicated it
This commit is contained in:
parent
75d0b66121
commit
4ce57f52a8
@ -4297,10 +4297,14 @@ public abstract interface class net/mamoe/mirai/message/data/Image : net/mamoe/m
|
||||
public static final field Key Lnet/mamoe/mirai/message/data/Image$Key;
|
||||
public static final field SERIAL_NAME Ljava/lang/String;
|
||||
public static fun fromId (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image;
|
||||
public abstract fun getHeight ()I
|
||||
public abstract fun getImageId ()Ljava/lang/String;
|
||||
public static fun getImageIdRegex ()Lkotlin/text/Regex;
|
||||
public static fun getImageResourceIdRegex1 ()Lkotlin/text/Regex;
|
||||
public static fun getImageResourceIdRegex2 ()Lkotlin/text/Regex;
|
||||
public abstract fun getImageType ()Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public abstract fun getSize ()J
|
||||
public abstract fun getWidth ()I
|
||||
public static fun queryUrl (Lnet/mamoe/mirai/message/data/Image;)Ljava/lang/String;
|
||||
public static fun queryUrl (Lnet/mamoe/mirai/message/data/Image;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
@ -4333,6 +4337,25 @@ public final class net/mamoe/mirai/message/data/Image$Serializer : kotlinx/seria
|
||||
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/Image;)V
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/ImageType : java/lang/Enum {
|
||||
public static final field APNG Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final field BMP Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final field Companion Lnet/mamoe/mirai/message/data/ImageType$Companion;
|
||||
public static final field GIF Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final field JPG Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final field PNG Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final field UNKNOWN Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final fun match (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final fun matchOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static fun values ()[Lnet/mamoe/mirai/message/data/ImageType;
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/ImageType$Companion {
|
||||
public final fun match (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public final fun matchOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/LightApp : net/mamoe/mirai/message/code/CodableMessage, net/mamoe/mirai/message/data/RichMessage {
|
||||
public static final field Key Lnet/mamoe/mirai/message/data/LightApp$Key;
|
||||
public static final field SERIAL_NAME Ljava/lang/String;
|
||||
|
@ -4297,10 +4297,14 @@ public abstract interface class net/mamoe/mirai/message/data/Image : net/mamoe/m
|
||||
public static final field Key Lnet/mamoe/mirai/message/data/Image$Key;
|
||||
public static final field SERIAL_NAME Ljava/lang/String;
|
||||
public static fun fromId (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image;
|
||||
public abstract fun getHeight ()I
|
||||
public abstract fun getImageId ()Ljava/lang/String;
|
||||
public static fun getImageIdRegex ()Lkotlin/text/Regex;
|
||||
public static fun getImageResourceIdRegex1 ()Lkotlin/text/Regex;
|
||||
public static fun getImageResourceIdRegex2 ()Lkotlin/text/Regex;
|
||||
public abstract fun getImageType ()Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public abstract fun getSize ()J
|
||||
public abstract fun getWidth ()I
|
||||
public static fun queryUrl (Lnet/mamoe/mirai/message/data/Image;)Ljava/lang/String;
|
||||
public static fun queryUrl (Lnet/mamoe/mirai/message/data/Image;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
@ -4333,6 +4337,25 @@ public final class net/mamoe/mirai/message/data/Image$Serializer : kotlinx/seria
|
||||
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/Image;)V
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/ImageType : java/lang/Enum {
|
||||
public static final field APNG Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final field BMP Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final field Companion Lnet/mamoe/mirai/message/data/ImageType$Companion;
|
||||
public static final field GIF Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final field JPG Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final field PNG Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final field UNKNOWN Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final fun match (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static final fun matchOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public static fun values ()[Lnet/mamoe/mirai/message/data/ImageType;
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/ImageType$Companion {
|
||||
public final fun match (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
|
||||
public final fun matchOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/LightApp : net/mamoe/mirai/message/code/CodableMessage, net/mamoe/mirai/message/data/RichMessage {
|
||||
public static final field Key Lnet/mamoe/mirai/message/data/LightApp$Key;
|
||||
public static final field SERIAL_NAME Ljava/lang/String;
|
||||
|
@ -85,6 +85,28 @@ public interface Image : Message, MessageContent, CodableMessage {
|
||||
*/
|
||||
public val imageId: String
|
||||
|
||||
/**
|
||||
* 图片的宽度 (px), 当无法获取时为 0
|
||||
*/
|
||||
public val width: Int
|
||||
|
||||
/**
|
||||
* 图片的高度 (px), 当无法获取时为 0
|
||||
*/
|
||||
public val height: Int
|
||||
|
||||
/**
|
||||
* 图片的大小(字节), 当无法获取时为 0
|
||||
*/
|
||||
public val size: Long
|
||||
|
||||
/**
|
||||
* 图片的类型, 当无法获取时为未知 [ImageType.UNKNOWN]
|
||||
*
|
||||
* @see ImageType
|
||||
*/
|
||||
public val imageType: ImageType
|
||||
|
||||
public object AsStringSerializer : KSerializer<Image> by String.serializer().mapPrimitive(
|
||||
SERIAL_NAME,
|
||||
serialize = { imageId },
|
||||
@ -188,6 +210,29 @@ public interface Image : Message, MessageContent, CodableMessage {
|
||||
@JvmSynthetic
|
||||
public inline fun Image(imageId: String): Image = Image.fromId(imageId)
|
||||
|
||||
public enum class ImageType {
|
||||
PNG,
|
||||
BMP,
|
||||
JPG,
|
||||
GIF,
|
||||
//WEBP, //Unsupported by pc client
|
||||
APNG,
|
||||
UNKNOWN;
|
||||
|
||||
public companion object {
|
||||
private val IMAGE_TYPE_ENUM_LIST = values()
|
||||
@JvmStatic
|
||||
public fun match(str: String): ImageType {
|
||||
return matchOrNull(str) ?: UNKNOWN
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun matchOrNull(str: String): ImageType? {
|
||||
val input = str.uppercase()
|
||||
return IMAGE_TYPE_ENUM_LIST.firstOrNull { it.name == input }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Internals
|
||||
@ -211,6 +256,15 @@ public val Image.md5: ByteArray
|
||||
public sealed class AbstractImage : Image {
|
||||
private val _stringValue: String? by lazy(NONE) { "[mirai:image:$imageId]" }
|
||||
|
||||
override val size: Long
|
||||
get() = 0L
|
||||
override val width: Int
|
||||
get() = 0
|
||||
override val height: Int
|
||||
get() = 0
|
||||
override val imageType: ImageType
|
||||
get() = ImageType.UNKNOWN
|
||||
|
||||
final override fun toString(): String = _stringValue!!
|
||||
final override fun contentToString(): String = "[图片]"
|
||||
override fun appendMiraiCodeTo(builder: StringBuilder) {
|
||||
|
@ -19,8 +19,9 @@ public val FILE_TYPES: MutableMap<String, String> = mutableMapOf(
|
||||
"FFD8FF" to "jpg",
|
||||
"89504E47" to "png",
|
||||
"47494638" to "gif",
|
||||
"49492A00" to "tif",
|
||||
//"49492A00" to "tif", // client doesn't support
|
||||
"424D" to "bmp",
|
||||
//"52494646" to "webp", // pc client doesn't support
|
||||
|
||||
// "57415645" to "wav", // server doesn't support
|
||||
|
||||
|
@ -18,9 +18,7 @@ import net.mamoe.mirai.data.UserInfo
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.internal.QQAndroidBot
|
||||
import net.mamoe.mirai.internal.message.OfflineFriendImage
|
||||
import net.mamoe.mirai.internal.message.contextualBugReportException
|
||||
import net.mamoe.mirai.internal.message.getImageType
|
||||
import net.mamoe.mirai.internal.message.*
|
||||
import net.mamoe.mirai.internal.network.components.BdhSession
|
||||
import net.mamoe.mirai.internal.network.highway.ChannelKind
|
||||
import net.mamoe.mirai.internal.network.highway.Highway
|
||||
@ -77,6 +75,7 @@ internal sealed class AbstractUser(
|
||||
if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
|
||||
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
|
||||
}
|
||||
val imageInfo = runBIO { resource.calculateImageInfo() }
|
||||
val resp = bot.network.run {
|
||||
LongConn.OffPicUp(
|
||||
bot.client,
|
||||
@ -86,7 +85,10 @@ internal sealed class AbstractUser(
|
||||
dstUin = this@AbstractUser.id,
|
||||
fileMd5 = resource.md5,
|
||||
fileSize = resource.size,
|
||||
fileName = resource.md5.toUHexString("") + "." + resource.formatName,
|
||||
imgWidth = imageInfo.width,
|
||||
imgHeight = imageInfo.height,
|
||||
imgType = getIdByImageType(imageInfo.imageType),
|
||||
fileName = "${resource.md5.toUHexString("")}.${resource.formatName}",
|
||||
imgOriginal = true,
|
||||
buildVer = bot.client.buildVer,
|
||||
),
|
||||
@ -99,22 +101,28 @@ internal sealed class AbstractUser(
|
||||
.takeIf { it != ExternalResource.DEFAULT_FORMAT_NAME }
|
||||
?: resource.formatName
|
||||
|
||||
OfflineFriendImage(
|
||||
imageId = generateImageIdFromResourceId(
|
||||
resourceId = resp.resourceId,
|
||||
format = imageType
|
||||
) ?: kotlin.run {
|
||||
if (resp.imageInfo.fileMd5.size == 16) {
|
||||
generateImageId(resp.imageInfo.fileMd5, imageType)
|
||||
} else {
|
||||
throw contextualBugReportException(
|
||||
"Failed to compute friend image image from resourceId: ${resp.resourceId}",
|
||||
resp._miraiContentToString(),
|
||||
additional = "并附加此时正在上传的文件"
|
||||
)
|
||||
}
|
||||
}
|
||||
).also {
|
||||
resp.imageInfo.run {
|
||||
OfflineFriendImage(
|
||||
imageId = generateImageIdFromResourceId(
|
||||
resourceId = resp.resourceId,
|
||||
format = imageType
|
||||
) ?: kotlin.run {
|
||||
if (resp.imageInfo.fileMd5.size == 16) {
|
||||
generateImageId(resp.imageInfo.fileMd5, imageType)
|
||||
} else {
|
||||
throw contextualBugReportException(
|
||||
"Failed to compute friend image image from resourceId: ${resp.resourceId}",
|
||||
resp._miraiContentToString(),
|
||||
additional = "并附加此时正在上传的文件"
|
||||
)
|
||||
}
|
||||
},
|
||||
width = fileWidth,
|
||||
height = fileHeight,
|
||||
imageType = getImageTypeById(fileType),
|
||||
size = resource.size
|
||||
)
|
||||
}.also {
|
||||
ImageUploadEvent.Succeed(this, resource, it).broadcast()
|
||||
}
|
||||
}
|
||||
@ -138,7 +146,10 @@ internal sealed class AbstractUser(
|
||||
uin = bot.id,
|
||||
groupCode = id,
|
||||
md5 = resource.md5,
|
||||
size = resource.size
|
||||
size = resource.size,
|
||||
picWidth = imageInfo.width,
|
||||
picHeight = imageInfo.height,
|
||||
picType = getIdByImageType(imageInfo.imageType),
|
||||
).sendAndExpect(bot)
|
||||
|
||||
when (response) {
|
||||
@ -205,9 +216,16 @@ internal sealed class AbstractUser(
|
||||
)
|
||||
}.getOrThrow()
|
||||
|
||||
OfflineFriendImage(
|
||||
generateImageIdFromResourceId(resp.resourceId, resource.formatName) ?: resp.resourceId
|
||||
).also {
|
||||
imageInfo.run {
|
||||
OfflineFriendImage(
|
||||
imageId = generateImageIdFromResourceId(resp.resourceId, resource.formatName)
|
||||
?: resp.resourceId,
|
||||
width = width,
|
||||
height = height,
|
||||
imageType = imageType,
|
||||
size = resource.size
|
||||
)
|
||||
}.also {
|
||||
ImageUploadEvent.Succeed(this, resource, it).broadcast()
|
||||
}
|
||||
}
|
||||
|
@ -23,8 +23,7 @@ import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.internal.QQAndroidBot
|
||||
import net.mamoe.mirai.internal.contact.announcement.AnnouncementsImpl
|
||||
import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
|
||||
import net.mamoe.mirai.internal.message.OfflineAudioImpl
|
||||
import net.mamoe.mirai.internal.message.OfflineGroupImage
|
||||
import net.mamoe.mirai.internal.message.*
|
||||
import net.mamoe.mirai.internal.network.components.BdhSession
|
||||
import net.mamoe.mirai.internal.network.handler.NetworkHandler
|
||||
import net.mamoe.mirai.internal.network.handler.logger
|
||||
@ -170,6 +169,7 @@ internal class GroupImpl constructor(
|
||||
if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
|
||||
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
|
||||
}
|
||||
val imageInfo = runBIO { resource.calculateImageInfo() }
|
||||
bot.network.run<NetworkHandler, Image> {
|
||||
val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp(
|
||||
bot.client,
|
||||
@ -177,6 +177,11 @@ internal class GroupImpl constructor(
|
||||
groupCode = id,
|
||||
md5 = resource.md5,
|
||||
size = resource.size,
|
||||
filename = "${resource.md5.toUHexString("")}.${resource.formatName}",
|
||||
picWidth = imageInfo.width,
|
||||
picHeight = imageInfo.height,
|
||||
picType = getIdByImageType(imageInfo.imageType),
|
||||
originalPic = 1
|
||||
).sendAndExpect()
|
||||
|
||||
when (response) {
|
||||
@ -187,8 +192,18 @@ internal class GroupImpl constructor(
|
||||
}
|
||||
is ImgStore.GroupPicUp.Response.FileExists -> {
|
||||
val resourceId = resource.calculateResourceId()
|
||||
return OfflineGroupImage(imageId = resourceId)
|
||||
.also { it.fileId = response.fileId.toInt() }
|
||||
return response.fileInfo.run {
|
||||
OfflineGroupImage(
|
||||
imageId = resourceId,
|
||||
height = fileHeight,
|
||||
width = fileWidth,
|
||||
imageType = getImageTypeById(fileType),
|
||||
size = resource.size
|
||||
)
|
||||
}
|
||||
.also {
|
||||
it.fileId = response.fileId.toInt()
|
||||
}
|
||||
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
|
||||
}
|
||||
is ImgStore.GroupPicUp.Response.RequireUpload -> {
|
||||
@ -208,8 +223,15 @@ internal class GroupImpl constructor(
|
||||
},
|
||||
)
|
||||
|
||||
return OfflineGroupImage(imageId = resource.calculateResourceId())
|
||||
.also { it.fileId = response.fileId.toInt() }
|
||||
return imageInfo.run {
|
||||
OfflineGroupImage(
|
||||
imageId = resource.calculateResourceId(),
|
||||
width = width,
|
||||
height = height,
|
||||
imageType = imageType,
|
||||
size = resource.size
|
||||
)
|
||||
}.also { it.fileId = response.fileId.toInt() }
|
||||
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
|
||||
}
|
||||
}
|
||||
|
@ -440,9 +440,19 @@ internal open class GroupSendMessageHandler(
|
||||
uin = bot.id,
|
||||
groupCode = id,
|
||||
md5 = image.md5,
|
||||
size = if (image is OnlineFriendImageImpl) image.delegate.fileLen else 0
|
||||
size = image.size
|
||||
).sendAndExpect()
|
||||
return OfflineGroupImage(image.imageId).also { img ->
|
||||
return OfflineGroupImage(
|
||||
imageId = image.imageId,
|
||||
width = image.width,
|
||||
height = image.height,
|
||||
size = if (response is ImgStore.GroupPicUp.Response.FileExists) {
|
||||
response.fileInfo.fileSize
|
||||
} else {
|
||||
image.size
|
||||
},
|
||||
imageType = image.imageType
|
||||
).also { img ->
|
||||
when (response) {
|
||||
is ImgStore.GroupPicUp.Response.FileExists -> {
|
||||
img.fileId = response.fileId.toInt()
|
||||
|
163
mirai-core/src/commonMain/kotlin/message/ImageDecoder.kt
Normal file
163
mirai-core/src/commonMain/kotlin/message/ImageDecoder.kt
Normal file
@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright 2019-2021 Mamoe Technologies and contributors.
|
||||
*
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.message
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import kotlinx.io.streams.asInput
|
||||
import net.mamoe.mirai.message.data.ImageType
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.readString
|
||||
import net.mamoe.mirai.utils.withUse
|
||||
import java.io.IOException
|
||||
|
||||
//SOF0-SOF3 SOF5-SOF7 SOF9-SOF11 SOF13-SOF15 Segment
|
||||
// (0xC4, 0xC8 and 0xCC not included due to is not an SOF)
|
||||
private val JPG_SOF_RANGE = listOf(
|
||||
0xC0.toByte()..0xC3.toByte(),
|
||||
0xC5.toByte()..0xC7.toByte(),
|
||||
0xC9.toByte()..0xCB.toByte(),
|
||||
0xCD.toByte()..0xCF.toByte()
|
||||
)
|
||||
|
||||
private fun Input.getJPGImageInfo(): ImageInfo {
|
||||
require(readBytes(2).contentEquals(byteArrayOf(0xFF.toByte(), 0xD8.toByte()))) {
|
||||
"It's not a valid jpg file"
|
||||
}
|
||||
//0xFF Segment Start
|
||||
while (readByte() == 0xFF.toByte()) {
|
||||
val type = readByte()
|
||||
//Find SOF
|
||||
if (JPG_SOF_RANGE.any { it.contains(type) }) {
|
||||
//Length
|
||||
discardExact(2)
|
||||
//Data precision
|
||||
discardExact(1)
|
||||
val height = readShort().toInt()
|
||||
val width = readShort().toInt()
|
||||
return ImageInfo(width = width, height = height, imageType = ImageType.JPG)
|
||||
} else {
|
||||
//SOS Segment, header is ended
|
||||
if (type == 0xDA.toByte()) {
|
||||
break
|
||||
}
|
||||
//Other segment, skip
|
||||
discardExact(
|
||||
//Skip size=segment length - 2 (length data itself)
|
||||
readShort().toInt() - 2
|
||||
)
|
||||
}
|
||||
}
|
||||
throw IllegalArgumentException("It's not a valid jpg file, failed to find an SOF segment")
|
||||
}
|
||||
|
||||
private fun Input.getBMPImageInfo(): ImageInfo {
|
||||
require(readString(2) == "BM") {
|
||||
"It's not a valid bmp file"
|
||||
}
|
||||
//==========
|
||||
//FILE HEADER
|
||||
//==========
|
||||
//Size
|
||||
discardExact(4)
|
||||
//Reserve 2*2bytes
|
||||
discardExact(4)
|
||||
//Offset for image data
|
||||
discardExact(4)
|
||||
//==========
|
||||
//INFO HEADER
|
||||
//==========
|
||||
//Size
|
||||
discardExact(4)
|
||||
return ImageInfo(
|
||||
width = readIntLittleEndian(),
|
||||
height = readIntLittleEndian(),
|
||||
imageType = ImageType.BMP
|
||||
)
|
||||
}
|
||||
|
||||
private fun Input.getPNGImageInfo(): ImageInfo {
|
||||
require(
|
||||
readBytes(8).contentEquals(
|
||||
byteArrayOf(
|
||||
0x89.toByte(),
|
||||
0x50,
|
||||
0x4e,
|
||||
0x47,
|
||||
0x0d,
|
||||
0x0a,
|
||||
0x1a,
|
||||
0x0a
|
||||
)
|
||||
)
|
||||
) {
|
||||
"It's not a valid png file"
|
||||
}
|
||||
//Chunk length
|
||||
discardExact(4)
|
||||
//Chunk type
|
||||
var type = readString(4)
|
||||
//First chunk must be IHDR
|
||||
require(type == "IHDR") {
|
||||
"It's not a valid png file, First chunk must be IHDR"
|
||||
}
|
||||
val width = readInt()
|
||||
val height = readInt()
|
||||
//Skip to next chunk
|
||||
//Bit depth (1 byte) + color type (1 byte)
|
||||
// + compression method (1 byte) + filter method (1 byte)
|
||||
// + interlace method (1 byte) + CRC(4 bytes) = 9 bytes
|
||||
discardExact(9)
|
||||
|
||||
//Chunk length
|
||||
discardExact(4)
|
||||
//Chunk type
|
||||
type = readString(4)
|
||||
|
||||
return ImageInfo(
|
||||
width = width,
|
||||
height = height,
|
||||
//Correct the image type
|
||||
//If is apng, it has to be an acTL chunk
|
||||
imageType = if (type == "acTL") {
|
||||
ImageType.APNG
|
||||
} else {
|
||||
ImageType.PNG
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun Input.getGIFImageInfo(): ImageInfo {
|
||||
|
||||
require(readString(6).run { startsWith("GIF") && endsWith("a") }) {
|
||||
"It's not a valid gif file"
|
||||
}
|
||||
return ImageInfo(
|
||||
width = readShortLittleEndian().toInt(),
|
||||
height = readShortLittleEndian().toInt(),
|
||||
imageType = ImageType.GIF
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, IllegalArgumentException::class)
|
||||
internal fun ExternalResource.calculateImageInfo(): ImageInfo {
|
||||
//Preload
|
||||
val imageType = ImageType.match(formatName)
|
||||
return inputStream().asInput().withUse {
|
||||
when (imageType) {
|
||||
ImageType.JPG -> getJPGImageInfo()
|
||||
ImageType.BMP -> getBMPImageInfo()
|
||||
ImageType.GIF -> getGIFImageInfo()
|
||||
ImageType.PNG, ImageType.APNG -> getPNGImageInfo()
|
||||
else -> {
|
||||
throw IllegalArgumentException("Unsupported image type for ExternalResource $this, considering use gif/png/bmp/jpg format.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,13 @@ internal class OnlineGroupImageImpl(
|
||||
) : OnlineGroupImage() {
|
||||
object Serializer : Image.FallbackSerializer("OnlineGroupImage")
|
||||
|
||||
override val size: Long get() = delegate.size.toLong()
|
||||
override val width: Int
|
||||
get() = delegate.width
|
||||
override val height: Int
|
||||
get() = delegate.height
|
||||
override val imageType: ImageType
|
||||
get() = getImageTypeById(delegate.imageType)
|
||||
|
||||
override val imageId: String = generateImageId(
|
||||
delegate.picMd5,
|
||||
@ -69,6 +76,13 @@ internal class OnlineFriendImageImpl(
|
||||
OnlineFriendImage() {
|
||||
object Serializer : Image.FallbackSerializer("OnlineFriendImage")
|
||||
|
||||
override val size: Long get() = delegate.fileLen
|
||||
override val width: Int
|
||||
get() = delegate.picWidth
|
||||
override val height: Int
|
||||
get() = delegate.picHeight
|
||||
override val imageType: ImageType
|
||||
get() = getImageTypeById(delegate.imgType)
|
||||
override val imageId: String = kotlin.run {
|
||||
val imageType = getImageType(delegate.imgType)
|
||||
generateImageIdFromResourceId(delegate.resId, imageType)
|
||||
@ -109,14 +123,38 @@ OnlineFriendImage() {
|
||||
|
||||
internal val UNKNOWN_IMAGE_TYPE_PROMPT_ENABLED = systemProp("mirai.unknown.image.type.logging", false)
|
||||
|
||||
internal fun getImageTypeById(id: Int): ImageType {
|
||||
return if (id == 2001) {
|
||||
ImageType.APNG
|
||||
} else {
|
||||
ImageType.match(getImageType(id))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getIdByImageType(imageType: ImageType): Int {
|
||||
return when (imageType) {
|
||||
ImageType.JPG -> 1000
|
||||
ImageType.PNG -> 1001
|
||||
//ImageType.WEBP -> 1002 //Unsupported by pc client
|
||||
ImageType.BMP -> 1005
|
||||
ImageType.GIF -> 2000
|
||||
ImageType.APNG -> 2001
|
||||
//default to jpg
|
||||
else -> 1000
|
||||
}
|
||||
}
|
||||
|
||||
internal data class ImageInfo(val width: Int = 0, val height: Int = 0, val imageType: ImageType = ImageType.UNKNOWN)
|
||||
|
||||
internal fun getImageType(id: Int): String {
|
||||
return when (id) {
|
||||
1000 -> "jpg"
|
||||
1001 -> "png"
|
||||
1002 -> "webp"
|
||||
//1002 -> "webp" //Unsupported by pc client
|
||||
1005 -> "bmp"
|
||||
2000 -> "gif"
|
||||
2001, 3 -> "png"
|
||||
2000, 3, 4 -> "gif"
|
||||
//apng
|
||||
2001 -> "png"
|
||||
else -> {
|
||||
if (UNKNOWN_IMAGE_TYPE_PROMPT_ENABLED) {
|
||||
Image.logger.debug(
|
||||
@ -134,16 +172,20 @@ internal fun ImMsgBody.NotOnlineImage.toCustomFace(): ImMsgBody.CustomFace {
|
||||
return ImMsgBody.CustomFace(
|
||||
filePath = generateImageId(picMd5, getImageType(imgType)),
|
||||
picMd5 = picMd5,
|
||||
bizType = 5,
|
||||
fileType = 66,
|
||||
useful = 1,
|
||||
flag = ByteArray(4),
|
||||
bigUrl = bigUrl,
|
||||
origUrl = origUrl,
|
||||
width = picWidth,
|
||||
height = picHeight,
|
||||
imageType = imgType,
|
||||
//_400Height = 235,
|
||||
//_400Url = "/gchatpic_new/000000000/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",
|
||||
//_400Width = 351,
|
||||
oldData = oldData,
|
||||
bizType = 66,
|
||||
useful = 1,
|
||||
origin = 1,
|
||||
origin = original,
|
||||
size = fileLen.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
@ -171,9 +213,15 @@ internal fun ImMsgBody.CustomFace.toNotOnlineImage(): ImMsgBody.NotOnlineImage {
|
||||
filePath = filePath,
|
||||
resId = resId,
|
||||
oldPicMd5 = false,
|
||||
picWidth = width,
|
||||
picHeight = height,
|
||||
imgType = imageType,
|
||||
picMd5 = picMd5,
|
||||
fileLen = size.toLong(),
|
||||
oldVerSendFile = oldData,
|
||||
downloadPath = resId,
|
||||
original = 1,
|
||||
original = origin,
|
||||
bizType = bizType,
|
||||
pbReserve = byteArrayOf(0x78, 0x02),
|
||||
)
|
||||
}
|
||||
@ -185,20 +233,26 @@ internal fun OfflineGroupImage.toJceData(): ImMsgBody.CustomFace {
|
||||
filePath = this.imageId,
|
||||
picMd5 = this.md5,
|
||||
flag = ByteArray(4),
|
||||
size = size.toInt(),
|
||||
width = width,
|
||||
height = height,
|
||||
imageType = getIdByImageType(imageType),
|
||||
origin = if (imageType == ImageType.GIF) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
},
|
||||
//_400Height = 235,
|
||||
//_400Url = "/gchatpic_new/000000000/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",
|
||||
//_400Width = 351,
|
||||
// pbReserve = "08 00 10 00 32 00 50 00 78 08".autoHexToBytes(),
|
||||
bizType = 66,
|
||||
bizType = 5,
|
||||
fileType = 66,
|
||||
useful = 1,
|
||||
origin = 1,
|
||||
// pbReserve = CustomFaceExtPb.ResvAttr().toByteArray(CustomFaceExtPb.ResvAttr.serializer())
|
||||
)
|
||||
}
|
||||
|
||||
private val oldData: ByteArray =
|
||||
"15 36 20 39 32 6B 41 31 00 38 37 32 66 30 36 36 30 33 61 65 31 30 33 62 37 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 30 31 45 39 34 35 31 42 2D 37 30 45 44 2D 45 41 45 33 2D 42 33 37 43 2D 31 30 31 46 31 45 45 42 46 35 42 35 7D 2E 70 6E 67 41".hexToBytes()
|
||||
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
internal fun OfflineFriendImage.toJceData(): ImMsgBody.NotOnlineImage {
|
||||
@ -208,8 +262,16 @@ internal fun OfflineFriendImage.toJceData(): ImMsgBody.NotOnlineImage {
|
||||
resId = friendImageId,
|
||||
oldPicMd5 = false,
|
||||
picMd5 = this.md5,
|
||||
fileLen = size,
|
||||
downloadPath = friendImageId,
|
||||
original = 1,
|
||||
original = if (imageType == ImageType.GIF) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
},
|
||||
picWidth = width,
|
||||
picHeight = height,
|
||||
imgType = getIdByImageType(imageType),
|
||||
pbReserve = byteArrayOf(0x78, 0x02)
|
||||
)
|
||||
}
|
||||
@ -246,6 +308,10 @@ internal interface OfflineImage : Image
|
||||
@Serializable(with = OfflineGroupImage.Serializer::class)
|
||||
internal data class OfflineGroupImage(
|
||||
override val imageId: String,
|
||||
override val width: Int = 0,
|
||||
override val height: Int = 0,
|
||||
override val size: Long = 0L,
|
||||
override val imageType: ImageType = ImageType.UNKNOWN
|
||||
) : GroupImage(), OfflineImage, DeferredOriginUrlAware {
|
||||
@Transient
|
||||
internal var fileId: Int? = null
|
||||
@ -288,6 +354,10 @@ internal val Image.friendImageId: String
|
||||
@Serializable(with = OfflineFriendImage.Serializer::class)
|
||||
internal data class OfflineFriendImage(
|
||||
override val imageId: String,
|
||||
override val width: Int = 0,
|
||||
override val height: Int = 0,
|
||||
override val size: Long = 0L,
|
||||
override val imageType: ImageType = ImageType.UNKNOWN
|
||||
) : FriendImage(), OfflineImage, DeferredOriginUrlAware {
|
||||
object Serializer : Image.FallbackSerializer("OfflineFriendImage")
|
||||
|
||||
|
@ -682,7 +682,7 @@ internal class ImMsgBody : ProtoBuf {
|
||||
@ProtoNumber(1) @JvmField val filePath: String = "",
|
||||
@ProtoNumber(2) @JvmField val fileLen: Long = 0L, // originally int
|
||||
@ProtoNumber(3) @JvmField val downloadPath: String = "",
|
||||
@ProtoNumber(4) @JvmField val oldVerSendFile: ByteArray = EMPTY_BYTE_ARRAY,
|
||||
@ProtoNumber(4) @JvmField val oldVerSendFile: ByteArray? = null,
|
||||
@ProtoNumber(5) @JvmField val imgType: Int = 0,
|
||||
@ProtoNumber(6) @JvmField val previewsImage: ByteArray = EMPTY_BYTE_ARRAY,
|
||||
@ProtoNumber(7) override val picMd5: ByteArray = EMPTY_BYTE_ARRAY,
|
||||
|
@ -47,7 +47,7 @@ internal class ImgStore {
|
||||
platformType: Int = 9,
|
||||
buType: Int = 2,
|
||||
appPicType: Int = 1006,
|
||||
originalPic: Int = 0
|
||||
originalPic: Int = 1
|
||||
) = buildOutgoingUniPacket(client) {
|
||||
writeProtoBuf(
|
||||
Cmd0x388.ReqBody.serializer(),
|
||||
|
95
mirai-core/src/commonTest/kotlin/message/ImageReadingTest.kt
Normal file
95
mirai-core/src/commonTest/kotlin/message/ImageReadingTest.kt
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2019-2021 Mamoe Technologies and contributors.
|
||||
*
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.message
|
||||
|
||||
import net.mamoe.mirai.internal.test.AbstractTest
|
||||
import net.mamoe.mirai.message.data.ImageType
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import net.mamoe.mirai.utils.hexToBytes
|
||||
import net.mamoe.mirai.utils.withUse
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.IOException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
internal class ImageReadingTest : AbstractTest() {
|
||||
@Test
|
||||
fun `test read apng`() {
|
||||
"89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 01 E0 00 00 01 90 08 06 00 00 00 76 F6 B3 54 00 00 00 08 61 63 54 4C 00 00 00 22 00 00 00 00 32 4C BC 74 00 00 00 1A 66 63 54 4C 00".testMatch(
|
||||
ImageType.APNG
|
||||
)
|
||||
assertFailsWith(IOException::class) { "89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00".testMatch(ImageType.APNG) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test read png`() {
|
||||
"89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 01 E0 00 00 01 90 08 06 00 00 00 76 F6 B3 54 00 00 00 01 73 52 47 42 00 AE".testMatch(
|
||||
ImageType.PNG
|
||||
)
|
||||
assertFailsWith(IOException::class) { "89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49".testMatch(ImageType.PNG) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test read gif`() {
|
||||
"47 49 46 38 39 61 E0 01 90 01 F7 FF 00 30 A3 B0".testMatch(ImageType.GIF)
|
||||
assertFailsWith(IOException::class) { "47 49 46 38 39 61 E0".testMatch(ImageType.GIF) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test read jpg`() {
|
||||
//SOF0
|
||||
"FF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 00 78 00 78 00 00 FF E1 00 5A 45 78 69 66 00 00 4D 4D 00 2A 00 00 00 08 00 05 03 01 00 05 00 00 00 01 00 00 00 4A 03 03 00 01 00 00 00 01 00 00 00 00 51 10 00 01 00 00 00 01 01 00 00 00 51 11 00 04 00 00 00 01 00 00 12 74 51 12 00 04 00 00 00 01 00 00 12 74 00 00 00 00 00 01 86 A0 00 00 B1 8F FF DB 00 43 00 02 01 01 02 01 01 02 02 02 02 02 02 02 02 03 05 03 03 03 03 03 06 04 04 03 05 07 06 07 07 07 06 07 07 08 09 0B 09 08 08 0A 08 07 07 0A 0D 0A 0A 0B 0C 0C 0C 0C 07 09 0E 0F 0D 0C 0E 0B 0C 0C 0C FF DB 00 43 01 02 02 02 03 03 03 06 03 03 06 0C 08 07 08 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C FF C0 00 11 08 01 90 01 E0 03 01 22 00 02 11 01 03 11 01 FF DA".testMatch(
|
||||
ImageType.JPG
|
||||
)
|
||||
//SOF2
|
||||
"FF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 00 78 00 78 00 00 FF E1 00 5A 45 78 69 66 00 00 4D 4D 00 2A 00 00 00 08 00 05 03 01 00 05 00 00 00 01 00 00 00 4A 03 03 00 01 00 00 00 01 00 00 00 00 51 10 00 01 00 00 00 01 01 00 00 00 51 11 00 04 00 00 00 01 00 00 12 74 51 12 00 04 00 00 00 01 00 00 12 74 00 00 00 00 00 01 86 A0 00 00 B1 8F FF DB 00 43 00 02 01 01 02 01 01 02 02 02 02 02 02 02 02 03 05 03 03 03 03 03 06 04 04 03 05 07 06 07 07 07 06 07 07 08 09 0B 09 08 08 0A 08 07 07 0A 0D 0A 0A 0B 0C 0C 0C 0C 07 09 0E 0F 0D 0C 0E 0B 0C 0C 0C FF DB 00 43 01 02 02 02 03 03 03 06 03 03 06 0C 08 07 08 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C FF C2 00 11 08 01 90 01 E0 03 01 22 00 02 11 01 03 11 01 FF DA".testMatch(
|
||||
ImageType.JPG
|
||||
)
|
||||
//Failed to find
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
"FF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 00 78 00 78 00 00 FF E1 00 5A 45 78 69 66 00 00 4D 4D 00 2A 00 00 00 08 00 05 03 01 00 05 00 00 00 01 00 00 00 4A 03 03 00 01 00 00 00 01 00 00 00 00 51 10 00 01 00 00 00 01 01 00 00 00 51 11 00 04 00 00 00 01 00 00 12 74 51 12 00 04 00 00 00 01 00 00 12 74 00 00 00 00 00 01 86 A0 00 00 B1 8F FF DB 00 43 00 02 01 01 02 01 01 02 02 02 02 02 02 02 02 03 05 03 03 03 03 03 06 04 04 03 05 07 06 07 07 07 06 07 07 08 09 0B 09 08 08 0A 08 07 07 0A 0D 0A 0A 0B 0C 0C 0C 0C 07 09 0E 0F 0D 0C 0E 0B 0C 0C 0C FF DB 00 43 01 02 02 02 03 03 03 06 03 03 06 0C 08 07 08 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C FF DA".testMatch(
|
||||
ImageType.JPG
|
||||
)
|
||||
}
|
||||
assertFailsWith(IOException::class) {
|
||||
"FF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 00 78 00 78 00 00 FF E1 00 5A".testMatch(
|
||||
ImageType.JPG
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test read bmp`() {
|
||||
"42 4D 36 CA 08 00 00 00 00 00 36 00 00 00 28 00 00 00 E0 01 00 00 90 01 00 00 01 00 18 00 00 00 00 00 00 CA 08 00 74 12 00 00 74 12 00 00 00 00 00 00 00 00 00 00"
|
||||
.testMatch(ImageType.BMP)
|
||||
assertFailsWith(IOException::class) {
|
||||
"42 4D 36 CA 08 00 00 00 00 00 36 00 00 00 28 00 00 00 E0 01 00 00 90".testMatch(
|
||||
ImageType.BMP
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test read fail`() {
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
"CA FE FE".testMatch(ImageType.BMP)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.testMatch(type: ImageType) {
|
||||
this.hexToBytes().toExternalResource().withUse {
|
||||
calculateImageInfo().run {
|
||||
assertEquals(480, width, "width")
|
||||
assertEquals(400, height, "height")
|
||||
assertEquals(type, imageType, "imageType")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user