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:
sandtechnology 2021-09-16 04:38:59 +08:00 committed by GitHub
parent 75d0b66121
commit 4ce57f52a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 528 additions and 49 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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.")
}
}
}
}

View File

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

View File

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

View File

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

View 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")
}
}
}
}