diff --git a/src/main/kotlin/DrawMeme.kt b/src/main/kotlin/DrawMeme.kt index c7dc95a..7b264a1 100644 --- a/src/main/kotlin/DrawMeme.kt +++ b/src/main/kotlin/DrawMeme.kt @@ -16,6 +16,7 @@ import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import net.mamoe.mirai.utils.info import org.laolittle.plugin.draw.Emoji.EmojiUtil.fullEmojiRegex import org.laolittle.plugin.draw.Emoji.EmojiUtil.toEmoji +import org.laolittle.plugin.draw.custom.initCustomMemes import org.laolittle.plugin.draw.meme.* import org.laolittle.plugin.sendImage import org.laolittle.plugin.toExternalResource @@ -35,6 +36,7 @@ object DrawMeme : KotlinPlugin( override fun onEnable() { logger.info { "Plugin loaded" } + initCustomMemes() val patReg = Regex("""^摸+([我爆头])?""") val choReg = Regex("#5(?:000|k)兆[\\s ]*(.+)") diff --git a/src/main/kotlin/DrawMemeEventChannel.kt b/src/main/kotlin/DrawMemeEventChannel.kt new file mode 100644 index 0000000..e86204a --- /dev/null +++ b/src/main/kotlin/DrawMemeEventChannel.kt @@ -0,0 +1,8 @@ +package org.laolittle.plugin.draw + +import kotlinx.coroutines.CoroutineExceptionHandler +import net.mamoe.mirai.event.globalEventChannel + +val drawMemeEventChannel = DrawMeme.globalEventChannel(CoroutineExceptionHandler { context, e -> + +}) \ No newline at end of file diff --git a/src/main/kotlin/FunctionConfig.kt b/src/main/kotlin/FunctionConfig.kt index 2ddb15d..b0b11e6 100644 --- a/src/main/kotlin/FunctionConfig.kt +++ b/src/main/kotlin/FunctionConfig.kt @@ -1,10 +1,11 @@ package org.laolittle.plugin.draw +import kotlinx.serialization.Serializable import net.mamoe.mirai.console.data.AutoSavePluginConfig import net.mamoe.mirai.console.data.value object FunctionConfig : AutoSavePluginConfig("FunctionConfig") { - @kotlinx.serialization.Serializable + @Serializable data class Configuration( val enable: Boolean = true, val groups: MutableSet = mutableSetOf() diff --git a/src/main/kotlin/General.kt b/src/main/kotlin/General.kt index 8abb4ff..bd5d922 100644 --- a/src/main/kotlin/General.kt +++ b/src/main/kotlin/General.kt @@ -10,8 +10,7 @@ import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.message.data.firstIsInstanceOrNull import net.mamoe.mirai.message.nextMessage import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource -import org.jetbrains.skia.Bitmap -import org.jetbrains.skia.Rect +import org.jetbrains.skia.* import java.nio.file.Path import kotlin.io.path.readBytes import org.jetbrains.skia.Image as SkImage @@ -66,4 +65,24 @@ internal suspend fun MessageEvent.getOrWaitImage(): Image? { }).firstIsInstanceOrNull() } -fun Bitmap.asImage() = org.jetbrains.skia.Image.makeFromBitmap(this) \ No newline at end of file +fun Bitmap.asImage() = org.jetbrains.skia.Image.makeFromBitmap(this) + +private val linearMipmap = FilterMipmap(FilterMode.LINEAR, MipmapMode.NEAREST) +fun Canvas.drawImageRectLinear( + image: org.jetbrains.skia.Image, + src: Rect, + dst: Rect, + paint: Paint?, + strict: Boolean +) = drawImageRect(image, src, dst, linearMipmap, paint, strict) + +fun Canvas.drawImageRectLinear(image: org.jetbrains.skia.Image, dst: Rect, paint: Paint?) = + drawImageRectLinear( + image, + Rect.makeWH(image.width.toFloat(), image.height.toFloat()), + dst, + paint, + true + ) + +fun Canvas.drawImageRectLinear(image: org.jetbrains.skia.Image, dst: Rect) = drawImageRectLinear(image, dst, null) \ No newline at end of file diff --git a/src/main/kotlin/custom/CustomMeme.kt b/src/main/kotlin/custom/CustomMeme.kt new file mode 100644 index 0000000..8e63757 --- /dev/null +++ b/src/main/kotlin/custom/CustomMeme.kt @@ -0,0 +1,213 @@ +package org.laolittle.plugin.draw.custom + +import kotlinx.coroutines.async +import org.jetbrains.skia.* +import org.laolittle.plugin.bytes +import org.laolittle.plugin.draw.DrawMeme +import org.laolittle.plugin.draw.drawImageRectLinear +import org.laolittle.plugin.gif.GifEncoder +import org.laolittle.plugin.gif.GifSetting +import java.io.File + +private val paintDefault = Paint() +class CustomMeme( + val name: String, + val input: Codec, +) { + private val surface = Surface.makeRaster(input.imageInfo) + + val isGif = input.frameCount > 1 + private val frames = kotlin.run { + if (!isGif) return@run arrayOf(input.readPixels()) + + Array(input.frameCount) { + Bitmap().apply { + allocPixels(input.imageInfo) + input.readPixels(this, it) + } + } + } + + private val _actions = ArrayList) -> Unit>>(input.frameCount) + val actions: List) -> Unit>> get() = _actions + + fun add(frame: Int, block: Canvas.(Array) -> Unit) { + _actions[frame].add(block) + } + + suspend fun makeImage(vararg images: Image): ByteArray { + if (!isGif) { + surface.canvas.clear(0) + surface.writePixels(frames[0], 0, 0) + actions.first().forEach { + surface.canvas.it(images) + } + return surface.makeImageSnapshot().bytes + } + + val (collector, writer) = GifEncoder.new( + GifSetting( + surface.width, + surface.height, + 100, + false, + GifSetting.Repeat.Finite(input.repetitionCount.toShort()) + ) + ) + + val bytes = DrawMeme.async { + writer.writeToBytes() + } + + var current = 0 + actions.forEachIndexed { index, each -> + surface.canvas.clear(0) + surface.writePixels(frames[index], 0, 0) + each.forEach { + surface.canvas.it(images) + } + val info = input.getFrameInfo(index) + + current += info.duration + collector.addFrame(surface.makeImageSnapshot().bytes, index, info.duration / 1000.0) + } + + return bytes.await() + } + + companion object { + fun fromFile(file: File): CustomMeme { + /*fun String.withIndexOf(string: String, startIndex: Int = 0, ignoreCase: Boolean = false, block: (Int) -> Unit): Boolean { + val index = indexOf(string,startIndex, ignoreCase) + + return index == 0 + }*/ + + val text = ArrayList(3) + + val reg = Regex("""//.*""") + + file.readLines().forEach { + if (it.isNotBlank()) { + val foo = it.replace(reg, "") + if (foo.isNotBlank()) text.add(foo.replace(',', ',').replace(':', ':')) + } + } + + var name: String? = null + var input: Codec? = null + val avatarVal = hashMapOf() + for (i in 0..2) { + val line = text[i] + when { + null == name && line.startsWith("meme:") -> { + val foo = line.replace(" ", "") + name = foo.slice(5..foo.lastIndex) + } + + null == input && line.startsWith("input:") -> { + val foo = line.indexOf('{') + val bar = line.indexOf('}') + val path = line.slice(foo + 1 until bar) + + input = Codec.makeFromData(Data.makeFromFileName(file.absoluteFile.parentFile.resolve(path).absolutePath)) + } + + avatarVal.isEmpty() && line.startsWith("avatars:") -> { + val foo = line + .replace(" ", "") + + + foo.slice(8..foo.lastIndex) + .split(',') + .forEachIndexed { index, s -> + if (!s.startsWith('@')) throw IllegalArgumentException("") + avatarVal[s.slice(1..s.lastIndex)] = index + } + } + } + } + + requireNotNull(name) + requireNotNull(input) + + val customMeme = CustomMeme(name, input) + + fun compile(input: String): Canvas.(Array) -> Unit { + val split = input.split(' ', limit = 3) + + return when (split.first()) { + "draw" -> { + val index = avatarVal[split[1]] ?: throw IllegalArgumentException("Compiler error: No such argument: ${split[1]}") + + val shape = split[2] + + val foo = shape.indexOf('{') + val bar = shape.indexOf('}') + val cons = shape.slice(foo + 1 until bar).split(',') + + when { + shape.startsWith("Rect", true) -> { + val rect = + Rect(cons[0].toFloat(), cons[1].toFloat(), cons[2].toFloat(), cons[3].toFloat()); + + { + val img = it[index] + + drawImageRectLinear(img, rect) + } + } + + shape.startsWith("Circle", true) -> { + val x = cons[0].toFloat() + val y = cons[1].toFloat() + val radius = cons[2].toFloat(); + + { + val img = it[index] + + // drawCircle(x, y, radius, paintTransparent) // 扫清障碍 + clipRRect(RRect.makeLTRB(x - radius, y - radius, x + radius, y + radius, radius), true) + drawImageRectLinear( + img, + Rect(x - radius, y - radius, x + radius, y + radius), + paintDefault + ) + } + } + + else -> { + throw IllegalArgumentException(input) + } + } + } + + else -> { + throw IllegalArgumentException(input) + } + } + } + + + var currentFrame = -1 + + for (i in 3..text.lastIndex) { + val line = text[i] + if (line.startsWith("frame")) { + currentFrame += 1 + continue + } + + customMeme.add(currentFrame, compile(line)) + } + + return customMeme + } + } + + init { + repeat(input.frameCount) { + _actions.add(mutableListOf()) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/custom/DrawCustom.kt b/src/main/kotlin/custom/DrawCustom.kt new file mode 100644 index 0000000..d039cfb --- /dev/null +++ b/src/main/kotlin/custom/DrawCustom.kt @@ -0,0 +1,48 @@ +package org.laolittle.plugin.draw.custom + +import io.ktor.client.request.* +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import net.mamoe.mirai.contact.Contact.Companion.sendImage +import net.mamoe.mirai.event.subscribeGroupMessages +import net.mamoe.mirai.message.data.At +import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource +import org.jetbrains.skia.Image +import org.laolittle.plugin.draw.DrawMeme +import org.laolittle.plugin.draw.drawMemeEventChannel +import org.laolittle.plugin.draw.httpClient +import java.io.File + +internal val customMemeFolder = DrawMeme.dataFolder.resolve("custom").also(File::mkdirs) + +internal val customMemes = mutableListOf() + +internal fun initCustomMemes() { + customMemeFolder.listFiles()?.forEach { + customMemes.add(CustomMeme.fromFile(it)) + } + + drawMemeEventChannel.subscribeGroupMessages { + customMemes.forEach { meme -> + val s = "#${meme.name}" + startsWith(s) { + val avatars = arrayListOf>() + message.forEach { m -> + if (m is At) { + avatars.add(DrawMeme.async { + val id = m.target + Image.makeFromEncoded(httpClient.get("https://q1.qlogo.cn/g?b=qq&nk=$id&s=640")) + }) + } + } + + meme.makeImage(*avatars.awaitAll().toTypedArray()) + .toExternalResource(if (meme.isGif) "GIF" else null).use { + subject.sendImage(it) + } + + } + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/Tests.kt b/src/test/kotlin/Tests.kt index 611ccd8..175b50f 100644 --- a/src/test/kotlin/Tests.kt +++ b/src/test/kotlin/Tests.kt @@ -1,12 +1,17 @@ package org.laolittle.plugin.draw -import org.jetbrains.skia.Image -import org.jetbrains.skia.ImageFilter -import org.jetbrains.skia.Paint -import org.jetbrains.skia.Surface +import kotlinx.coroutines.runBlocking +import okhttp3.internal.toHexString +import org.jetbrains.skia.* +import org.laolittle.plugin.bytes +import org.laolittle.plugin.draw.custom.CustomMeme +import java.io.File import kotlin.io.path.Path +import kotlin.io.path.listDirectoryEntries import kotlin.io.path.readBytes import kotlin.io.path.writeBytes +import kotlin.random.Random +import kotlin.system.measureTimeMillis import kotlin.test.Test class Tests { @@ -21,20 +26,121 @@ class Tests { //imageFilter = ImageFilter.makeBlur(0f, 10f, FilterTileMode.MIRROR) //imageFilter = ImageFilter.makeDilate(5f, 0f, null, null) //imageFilter = ImageFilter.makeErode(5f, 0f, null, null) - + this.maskFilter }) - drawImage(image, 0f, 0f, Paint().apply { - alpha = 125 + /*drawImage(image, 0f, 0f, Paint().apply { + //maskFilter = MaskFilter.makeBlur(FilterBlurMode.INNER, 0f, true) imageFilter = ImageFilter.makeErode(5f, 0f, null, null) - }) + })*/ } }.makeImageSnapshot().encodeToData()?.let { Path("outblur.png").writeBytes(it.bytes) } } + @Test + fun bit() { + val a = 0xffffffff + + println(a.toInt().toString(2)) + println(a.toInt().toHexString()) + } + + @Test + fun bba() { + val image = Image.makeFromEncoded(Path("atri.jpg").readBytes()) + + fun random(): Float { + return Random.nextDouble(-15.0,15.0).toFloat() + } + + Surface.makeRaster(image.imageInfo).apply { + + canvas.apply { + drawImage(image, 0f, 0f) + drawImage(image, random(), random(), Paint().apply { + alpha = 100 + colorFilter = ColorFilter.makeMatrix(ColorMatrix( + 1F,0F,0.2F,0.3F,0F, + 0F,0F,0F,0F,0F, + 0F,0F,0F,0F,0F, + 0F,0F,0F,1F,0F, + )) + }) + } + }.makeImageSnapshot().encodeToData()?.let { + Path("outfi.png").writeBytes(it.bytes) + } + } + + @Test + fun blue() { + val image = Image.makeFromEncoded(Path("atri.jpg").readBytes()) + + Surface.makeRaster(image.imageInfo).apply { + + canvas.apply { + drawImage(image, 0f, 0f, Paint().apply { + colorFilter = ColorFilter.makeMatrix(ColorMatrix( + //R G B A ? + 1F,0F,0F,0F,0F, // R + 0F,1F,0F,0F,0F, // G + 0.5F,0.6F,1F,.3F,0F, // B + 0F,0F,0F,1F,0F, // A + )) + }) + } + }.makeImageSnapshot().encodeToData()?.let { + Path("outblue.png").writeBytes(it.bytes) + } + } + + @Test + fun pic() { + val image = Image.makeFromEncoded(Path("atri.jpg").readBytes()) + val recorder = PictureRecorder().apply { + beginRecording(image.imageInfo.bounds.toRect()) + } + + recorder.recordingCanvas?.apply { + drawCircle(0f,0f,100f, Paint()) + drawImage(image, 0f,0f) + + } + + val pic = recorder.finishRecordingAsPicture() + pic.serializeToData().bytes.also(Path("pic.bin")::writeBytes) + + Surface.makeRaster(image.imageInfo).apply { + canvas.drawImage(image,0f,0f) + canvas.drawPicture(pic) + // pic.playback(canvas) + }.makeImageSnapshot().bytes.also(Path("outpic.png")::writeBytes) + } + + @Test + fun customMeme() { + measureTimeMillis { + val image = Image.makeFromEncoded(Path("atri.jpg").readBytes()) + val a = CustomMeme.fromFile(File("testmeme.txt")) + a.isGif + a.input + a.actions + a.name + runBlocking { + a.makeImage(image).also(Path("custom.png")::writeBytes) + } + } + } + + @Test + fun path() { + Path(".idea").listDirectoryEntries().forEach { + println(it) + } + } /*@Test fun arrayList() { val a = arrayListOf(12f)