-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
Copy pathImage.kt
450 lines (404 loc) · 14.3 KB
/
Image.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
/*
* Copyright 2019-2022 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
*/
@file:JvmMultifileClass
@file:JvmName("MessageUtils")
@file:Suppress(
"EXPERIMENTAL_API_USAGE",
"unused",
"UnusedImport",
"DEPRECATION_ERROR", "NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate"
)
package net.mamoe.mirai.message.data
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.Bot
import net.mamoe.mirai.IMirai
import net.mamoe.mirai.Mirai
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Contact.Companion.sendImage
import net.mamoe.mirai.contact.Contact.Companion.uploadImage
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.message.data.Image.Key.IMAGE_ID_REGEX
import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_1
import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_2
import net.mamoe.mirai.message.data.Image.Key.queryUrl
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
/**
* 自定义表情 (收藏的表情) 和普通图片.
*
*
* 最推荐的存储方式是存储图片原文件, 每次发送图片时都使用文件上传.
* 在上传时服务器会根据其缓存情况回复已有的图片 ID 或要求客户端上传. 详见 [Contact.uploadImage]
*
* ### 根据 ID 构造图片
* - [Image.fromId]. 在 Kotlin, 更推荐使用顶层函数 `val image = Image("id")`
*
* ### 上传和发送图片
* - [Contact.uploadImage] 上传 [资源文件][ExternalResource] 并得到 [Image] 消息
* - [Contact.sendImage] 上传 [资源文件][ExternalResource] 并发送返回的 [Image] 作为一条消息
*
* - [ExternalResource.uploadAsImage]
* - [ExternalResource.sendAsImageTo]
* - [Contact.sendImage]
*
* ### 下载图片
* - [Image.queryUrl] 扩展函数. 查询图片下载链接
* - [IMirai.queryImageUrl] 查询图片下载链接 (Java 使用)
*
* ## mirai 码支持
* 格式: [mirai:image:*[Image.imageId]*]
*
* @see FlashImage 闪照
* @see Image.flash 转换普通图片为闪照
*/
@Serializable(Image.Serializer::class)
@NotStableForInheritance
public interface Image : Message, MessageContent, CodableMessage {
/**
* 图片的 id.
*
* 图片 id 不一定会长时间保存, 也可能在将来改变格式, 因此不建议使用 id 发送图片.
*
* ### 格式
* 所有图片的 id 都满足正则表达式 [IMAGE_ID_REGEX]. 示例: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.ext` (ext 为文件后缀, 如 png)
*
* @see Image 使用 id 构造图片
*/
public val imageId: String
/**
* 图片的宽度 (px), 当无法获取时为 0
*
* @since 2.8.0
*/
public val width: Int
/**
* 图片的高度 (px), 当无法获取时为 0
*
* @since 2.8.0
*/
public val height: Int
/**
* 图片的大小(字节), 当无法获取时为 0. 可用于 [isUploaded].
*
* @since 2.8.0
*/
public val size: Long
/**
* 图片的类型, 当无法获取时为未知 [ImageType.UNKNOWN]
*
* @since 2.8.0
*
* @see ImageType
*/
public val imageType: ImageType
/**
* 判断该图片是否为 `动画表情`
*
* @since 2.8.0
*/
public val isEmoji: Boolean get() = false
/**
* 图片文件 MD5. 可用于 [isUploaded].
*
* @return 16 bytes
* @see isUploaded
* @since 2.9.0
*/ // was an extension on Image before 2.9.0-M1.
public val md5: ByteArray get() = calculateImageMd5ByImageId(imageId)
public object AsStringSerializer : KSerializer<Image> by String.serializer().mapPrimitive(
SERIAL_NAME,
serialize = { imageId },
deserialize = { Image(it) },
)
public object Serializer : KSerializer<Image> by FallbackSerializer("Image")
@MiraiInternalApi
public open class FallbackSerializer(serialName: String) : KSerializer<Image> by Delegate.serializer().map(
buildClassSerialDescriptor(serialName) { element("imageId", String.serializer().descriptor) },
serialize = { Delegate(imageId) },
deserialize = { Image(imageId) },
) {
@SerialName(SERIAL_NAME)
@Serializable
internal data class Delegate(
val imageId: String
)
}
/**
* [Image] 构建器.
*
* 示例:
*
* ```java
* Builder builder = Image.Builder.newBuilder("{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.jpg")
* builder.setSize(123);
* builder.setType(ImageType.PNG);
*
* Image image = builder.build();
* ```
*
* @since 2.9.0
*/
public class Builder private constructor(
/**
* @see Image.imageId
*/
public var imageId: String,
) {
/**
* 图片大小字节数. 如果不提供改属性, 将无法 [Image.Key.isUploaded]
*
* @see Image.size
*/
public var size: Long = 0
/**
* @see Image.imageType
*/
public var type: ImageType = ImageType.UNKNOWN
/**
* @see Image.width
*/
public var width: Int = 0
/**
* @see Image.height
*/
public var height: Int = 0
/**
* @see Image.isEmoji
*/
public var isEmoji: Boolean = false
/**
* 使用当前参数构造 [Image].
*/
public fun build(): Image = InternalImageProtocol.instance.createImage(
imageId = imageId,
size = size,
type = type,
width = width,
height = height,
isEmoji = isEmoji,
)
public companion object {
/**
* 创建一个 [Builder]
*/
@JvmStatic
public fun newBuilder(imageId: String): Builder = Builder(imageId)
}
}
@JvmBlockingBridge
public companion object Key : AbstractMessageKey<Image>({ it.safeCast() }) {
public const val SERIAL_NAME: String = "Image"
/**
* 通过 [Image.imageId] 构造一个 [Image] 以便发送.
*
* 图片 ID 不一定会长时间保存, 因此不建议使用 ID 发送图片. 建议使用 [Builder], 可以指定更多参数 (以及用于查询图片是否存在于服务器的必要参数 size).
*
* @see Image 获取更多说明
* @see Image.imageId 获取更多说明
* @see Builder
*/
@JvmStatic
public fun fromId(imageId: String): Image = Mirai.createImage(imageId)
/**
* 构造一个 [Image.Builder] 实例.
*
* @since 2.9.0
*/
@JvmStatic
public fun newBuilder(imageId: String): Builder = Builder.newBuilder(imageId)
/**
* 查询原图下载链接.
*
* - 当图片为从服务器接收的消息中的图片时, 可以直接获取下载链接, 本函数不会挂起协程.
* - 其他情况下协程可能会挂起并向服务器查询下载链接, 或不挂起并拼接一个链接.
*
* @return 原图 HTTP 下载链接
* @throws IllegalStateException 当无任何 [Bot] 在线时抛出 (因为无法获取相关协议)
*/
@JvmStatic
public suspend fun Image.queryUrl(): String {
val bot = Bot.instancesSequence.firstOrNull() ?: error("No Bot available to query image url")
return Mirai.queryImageUrl(bot, this)
}
/**
* 当图片在服务器上存在时返回 `true`, 这意味着图片可以直接发送给 [contact].
*
* 若返回 `false`, 则图片需要用 [ExternalResource] 重新上传 ([Contact.uploadImage]).
*
* @since 2.9.0
*/
@JvmStatic
public suspend fun Image.isUploaded(bot: Bot): Boolean =
InternalImageProtocol.instance.isUploaded(bot, md5, size, null, imageType, width, height)
/**
* 当图片在服务器上存在时返回 `true`, 这意味着图片可以直接发送给 [contact].
*
* 若返回 `false`, 则图片需要用 [ExternalResource] 重新上传 ([Contact.uploadImage]).
*
* @param md5 图片文件 MD5. 可通过 [Image.md5] 获得.
* @param size 图片文件大小.
*
* @since 2.9.0
*/
@JvmStatic
public suspend fun isUploaded(
bot: Bot,
md5: ByteArray,
size: Long,
): Boolean = InternalImageProtocol.instance.isUploaded(bot, md5, size, null)
/**
* 由 [Image.imageId] 计算 [Image.md5].
*
* @since 2.9.0
*/
public fun calculateImageMd5ByImageId(imageId: String): ByteArray {
@Suppress("DEPRECATION")
return when {
imageId matches IMAGE_ID_REGEX -> imageId.imageIdToMd5(1)
imageId matches IMAGE_RESOURCE_ID_REGEX_2 -> imageId.imageIdToMd5(imageId.skipToSecondHyphen() + 1)
imageId matches IMAGE_RESOURCE_ID_REGEX_1 -> imageId.imageIdToMd5(1)
else -> throw IllegalArgumentException(
"Illegal imageId: '$imageId'. $ILLEGAL_IMAGE_ID_EXCEPTION_MESSAGE"
)
}
}
/**
* 统一 ID 正则表达式
*
* `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.ext`
*/
@Suppress("RegExpRedundantEscape") // This is required on Android
@JvmStatic
@get:JvmName("getImageIdRegex")
public val IMAGE_ID_REGEX: Regex =
Regex("""\{[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\}\..{3,5}""")
/**
* 图片资源 ID 正则表达式 1. mirai 内部使用.
*
* `/f8f1ab55-bf8e-4236-b55e-955848d7069f`
* @see IMAGE_RESOURCE_ID_REGEX_2
*/
@JvmStatic
@MiraiInternalApi
@get:JvmName("getImageResourceIdRegex1")
public val IMAGE_RESOURCE_ID_REGEX_1: Regex =
Regex("""/[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}""")
/**
* 图片资源 ID 正则表达式 2. mirai 内部使用.
*
* `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206`
* @see IMAGE_RESOURCE_ID_REGEX_1
*/
@JvmStatic
@MiraiInternalApi
@get:JvmName("getImageResourceIdRegex2")
public val IMAGE_RESOURCE_ID_REGEX_2: Regex =
Regex("""/[0-9]*-[0-9]*-[0-9a-fA-F]{32}""")
}
}
/**
* 通过 [Image.imageId] 构造一个 [Image] 以便发送.
*
* 图片 ID 不一定会长时间保存, 因此不建议使用 ID 发送图片. 建议使用 [Image.Builder], 可以指定更多参数 (以及用于查询图片是否存在于服务器的必要参数 size).
*
* @see Image 获取更多关于 [Image] 的说明
* @see Image.Builder 获取更多关于构造 [Image] 的方法
*
* @see IMirai.createImage
*/
@JvmSynthetic
public inline fun Image(imageId: String): Image = Image.Builder.newBuilder(imageId).build()
/**
* 使用 [Image.Builder] 构建一个 [Image].
*
* @see Image.Builder
* @since 2.9.0
*/
@JvmSynthetic
public inline fun Image(imageId: String, builderAction: Image.Builder.() -> Unit = {}): Image =
Image.Builder.newBuilder(imageId).apply(builderAction).build()
public enum class ImageType(
/**
* @since 2.9.0
*/
@MiraiInternalApi public val formatName: String,
@MiraiInternalApi public vararg val secondaryNames: String
) {
PNG("png"),
BMP("bmp"),
JPG("jpg", "JPEG", "JPE"),
GIF("gif"),
//WEBP, //Unsupported by pc client
APNG("png"),
UNKNOWN("gif"); // bad design, should use `null` to represent unknown, but we cannot change it anymore.
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 || it.secondaryNames.contains(input) }
}
}
}
///////////////////////////////////////////////////////////////////////////
// Internals
///////////////////////////////////////////////////////////////////////////
@Deprecated("Use member function", level = DeprecationLevel.HIDDEN) // safe since it was internal
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
@MiraiInternalApi
@get:JvmName("calculateImageMd5")
@DeprecatedSinceMirai(hiddenSince = "2.9")
public val Image.md5: ByteArray
get() = Image.calculateImageMd5ByImageId(imageId)
/**
* 内部图片协议实现
* @since 2.9.0-M1
*/
@MiraiInternalApi
public interface InternalImageProtocol { // naming it Internal* to assign it a lower priority when resolving Image*
public fun createImage(
imageId: String,
size: Long,
type: ImageType = ImageType.UNKNOWN,
width: Int = 0,
height: Int = 0,
isEmoji: Boolean = false
): Image
/**
* @param context 用于检查的 [Contact]. 群图片与好友图片是两个通道, 建议使用欲发送到的 [Contact] 对象作为 [contact] 参数, 但目前不提供此参数时也可以检查.
*/
public suspend fun isUploaded(
bot: Bot,
md5: ByteArray,
size: Long,
context: Contact? = null,
type: ImageType = ImageType.UNKNOWN,
width: Int = 0,
height: Int = 0
): Boolean
@MiraiInternalApi
public companion object {
public val instance: InternalImageProtocol by lazy {
loadService(
InternalImageProtocol::class,
"net.mamoe.mirai.internal.message.InternalImageProtocolImpl"
)
}
}
}