diff --git a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api index e676ca9ba..c661c70b8 100644 --- a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api +++ b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api @@ -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; diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index 67af440a4..95f09e197 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -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; diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt b/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt index 4977aecc5..359e68416 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt @@ -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 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) { diff --git a/mirai-core-utils/src/commonMain/kotlin/Files.kt b/mirai-core-utils/src/commonMain/kotlin/Files.kt index 3b453dbd3..db01d83b1 100644 --- a/mirai-core-utils/src/commonMain/kotlin/Files.kt +++ b/mirai-core-utils/src/commonMain/kotlin/Files.kt @@ -19,8 +19,9 @@ public val FILE_TYPES: MutableMap = 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 diff --git a/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt b/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt index 6cc920741..2b0882374 100644 --- a/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt +++ b/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt @@ -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() } } diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index 6f0ceabb0..a19b0b157 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -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 { 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() } } } diff --git a/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt b/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt index 3e5d73adc..3a208b534 100644 --- a/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt @@ -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() diff --git a/mirai-core/src/commonMain/kotlin/message/ImageDecoder.kt b/mirai-core/src/commonMain/kotlin/message/ImageDecoder.kt new file mode 100644 index 000000000..040b7c14e --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/ImageDecoder.kt @@ -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.") + } + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/imagesImpl.kt b/mirai-core/src/commonMain/kotlin/message/imagesImpl.kt index 855dd6c39..9a6e6322e 100644 --- a/mirai-core/src/commonMain/kotlin/message/imagesImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/imagesImpl.kt @@ -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") diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt index e851a5a80..0e6d02485 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt @@ -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, diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/image/ImgStore.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/image/ImgStore.kt index 077a84b99..4086eb9b2 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/image/ImgStore.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/image/ImgStore.kt @@ -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(), diff --git a/mirai-core/src/commonTest/kotlin/message/ImageReadingTest.kt b/mirai-core/src/commonTest/kotlin/message/ImageReadingTest.kt new file mode 100644 index 000000000..5ab1751ac --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/message/ImageReadingTest.kt @@ -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") + } + } + } +} \ No newline at end of file