Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

支持 WEBP 格式图片发送 #1584

Open
zhaodice opened this issue Sep 26, 2021 · 35 comments
Open

支持 WEBP 格式图片发送 #1584

zhaodice opened this issue Sep 26, 2021 · 35 comments
Labels
N 优先级: 一般 s:core 子系统: mirai-core t:feature 类型: 新特性
Milestone

Comments

@zhaodice
Copy link
Contributor

zhaodice commented Sep 26, 2021

版本:Mirai 2.8.0-M1
复现方式:发送webp格式的图片
复现结果:considering use gif/png/bmp/jpg
补充:在2.8.0-M1之前是可以正常发送webp图片的

星星.zip

@zhaodice zhaodice added the x:question 标签: 需要更多信息 label Sep 26, 2021
@zhaodice

This comment has been minimized.

@0f-0b
Copy link

0f-0b commented Sep 26, 2021

这像是 webp。

@sandtechnology
Copy link
Collaborator

这确实不是jpg照片 而是RIFF格式图片

@zhaodice
Copy link
Contributor Author

这确实不是jpg照片 而是RIFF格式图片

可恶,我被骗了,那...支持吗?

@zhaodice
Copy link
Contributor Author

之前的版本,是可以发送这个图片的,所以我就...没有实际验证它的真实格式,就想当然觉得是jpg了,哈哈哈

@sandtechnology
Copy link
Collaborator

这是WEBP格式 是以RIFF数据格式进行表示的 如果可以正常发送的话 应该是服务器进行了转换操作

@zhaodice
Copy link
Contributor Author

这是WEBP格式 是以RIFF数据格式进行表示的 如果可以正常发送的话 应该是服务器进行了转换操作

那mirai只需要把这个格式加白名单,就可以正常发送了吧(猜想)

@zhaodice zhaodice changed the title 某个特定的jpg无法被识别,报错:considering use gif/png/bmp/jpg 发送webp图片时报错:considering use gif/png/bmp/jpg Sep 26, 2021
@sandtechnology
Copy link
Collaborator

服务器并不支持webp图片 所以无法添加支持 如果支持的话就会有人来问为什么动态的webp图片发送之后不动了(

@zhaodice
Copy link
Contributor Author

服务器并不支持webp图片 所以无法添加支持 如果支持的话就会有人来问为什么动态的webp图片发送之后不动了(

那我先考虑本地写个本地转换库吧,静态图片应该不难

@sandtechnology
Copy link
Collaborator

sandtechnology commented Sep 26, 2021

关于WEBP图片的识别
R I F F (ASCII 4 bytes)
文件大小(小端序 unsigned int)
W E B P (ASCII 4 bytes)

Ref: https://developers.google.com/speed/webp/docs/riff_container#webp_file_header

另:webp的读取需要第三方库

@TheSnowfield
Copy link
Contributor

TheSnowfield commented Sep 26, 2021

关于WEBP图片的识别
R I F F (ASCII 4 bytes)
文件大小(小端序 unsigned int)
W E B P (ASCII 4 bytes)

Ref: https://developers.google.com/speed/webp/docs/riff_container#webp_file_header

另:webp的读取需要第三方库

不需要三方库, highway直接发送即可

参考示例

// RIFF
buffer.PeekUintBE(0, out value);
if (value == 0x52494646)
{
    // WEBP
    buffer.PeekUintBE(8, value);
    if (value == 0x57454250)
    {
        type = ImageFormat.WEBP;
        width = buffer.PeekUintLE(0x12);
        height = buffer.PeekUintLE(0x16);
    }
}

@sandtechnology
Copy link
Collaborator

sandtechnology commented Sep 26, 2021 via email

@TheSnowfield
Copy link
Contributor

问题是对WEBP格式的大小的获取,另外如果由highway直接发送的话指定的文件格式将会是不确定的,也无法提供对应的大小

为什么是不确定的
highway发送图片只需要 图像格式 MD5 分辨率 数据长度 数据

@zhaodice
Copy link
Contributor Author

zhaodice commented Sep 26, 2021

问题是对WEBP格式的大小的获取,另外如果由highway直接发送的话指定的文件格式将会是不确定的,也无法提供对应的大小

可否提供一个接口,可以让开发者自主实现获取该文件格式的大小,如果开发者不实现该接口,则按图片大小未知的方式往服务器发送,安卓的BitmapFactory.decodeByteArray挺方便的

@sandtechnology
Copy link
Collaborator

图像格式

图像格式如果是 webp,那最后发送的图片将会不被PC客户端识别(如果指定了图像格式为webp)

@zhaodice
Copy link
Contributor Author

我打算先本地用BitmapFactory库webp转jpg发送解决这个问题

@sandtechnology
Copy link
Collaborator

sandtechnology commented Sep 26, 2021

问题是对WEBP格式的大小的获取,另外如果由highway直接发送的话指定的文件格式将会是不确定的,也无法提供对应的大小

可否提供一个接口,可以让开发者自主实现获取该文件格式的大小,如果开发者不实现该接口,则按图片大小未知的方式往服务器发送,安卓的BitmapFactory.decodeByteArray挺方便的

实际上这是由协议限定的图片格式,之前能正常发送的原因的话我也不知道为什么(
@TheSnowfield 指定 webp 的格式发送之后PC客户端可以识别吗

@sandtechnology
Copy link
Collaborator

问题是对WEBP格式的大小的获取,另外如果由highway直接发送的话指定的文件格式将会是不确定的,也无法提供对应的大小

可否提供一个接口,可以让开发者自主实现获取该文件格式的大小,如果开发者不实现该接口,则按图片大小未知的方式往服务器发送,安卓的BitmapFactory.decodeByteArray挺方便的

不过倒是可以提供一个resolver,就负责转换为支持的格式类型

@TheSnowfield
Copy link
Contributor

图像格式

图像格式如果是 webp,那最后发送的图片将会不被PC客户端识别(如果指定了图像格式为webp)

PC不行, 但是安卓可以.

@sandtechnology
Copy link
Collaborator

这就是我没写WEBP的原因(躺
事实上安卓客户端在发送的时候会将webp转换为jpg发送,你可以试试(
关于解决这个问题...可以像我前文说的那样提供一个resolver来解决

@TheSnowfield
Copy link
Contributor

问题是对WEBP格式的大小的获取,另外如果由highway直接发送的话指定的文件格式将会是不确定的,也无法提供对应的大小

可否提供一个接口,可以让开发者自主实现获取该文件格式的大小,如果开发者不实现该接口,则按图片大小未知的方式往服务器发送,安卓的BitmapFactory.decodeByteArray挺方便的

不过倒是可以提供一个resolver,就负责转换为支持的格式类型

转换格式个人感觉没什么必要, 直接加上webp发送就行.
本来tx就没怎么用webp吧, 能发但是部分客户端支援不佳也属于情理之中.
要么就直接用支援较好的格式, 要么就自己去转换. 🤔

@sandtechnology
Copy link
Collaborator

转换格式个人感觉没什么必要, 直接加上webp发送就行.
本来tx就没怎么用webp吧, 能发但是部分客户端支援不佳也属于情理之中.
要么就直接用支援较好的格式, 要么就自己去转换. 🤔

emmmm 个人设计API的时候会考虑预期结果 在上传图片的时候肯定是预期所有客户端都支持的 所以就没有允许另外的格式
或者 问问项目维护者的意见? @Him188 @Karlatemp

@zhaodice
Copy link
Contributor Author

zhaodice commented Sep 26, 2021

也可以判断平台,如果是JVM平台,就用JVM自带的图片处理,如果是安卓平台,我已经写好了实现(土掉渣的反射,调用安卓的API)

    fun decodeWebP(encoded: ByteArray): ByteArray? {
        var result:ByteArray? = null
        val bitmapFactoryClazz = Class.forName("android.graphics.BitmapFactory")
        val bitmapClazz = Class.forName("android.graphics.Bitmap")
        val compressFormatClazz = Class.forName("android.graphics.Bitmap\$CompressFormat")
        val decodeByteArrayMethod = bitmapFactoryClazz.getMethod("decodeByteArray", ByteArray::class.java, Int::class.java, Int::class.java) // public static Bitmap decodeByteArray(byte[] data, int offset, int length) {
        val compressMethod = bitmapClazz.getMethod("compress", compressFormatClazz, Int::class.java, OutputStream::class.java)
        val recycleMethod = bitmapClazz.getMethod("recycle")
        val bitmap = decodeByteArrayMethod.invoke(null,encoded, 0, encoded.size)//BitmapFactory.decodeByteArray(encoded, 0, encoded.size)
        val byteArrayOutputStream = ByteArrayOutputStream()
        try {
            val bos = BufferedOutputStream(byteArrayOutputStream)
            compressMethod.invoke(bitmap, compressFormatClazz.getField("PNG").get(null), 100, bos)//bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos)
            bos.flush()
            result = byteArrayOutputStream.toByteArray()
            bos.close()
            recycleMethod.invoke(bitmap)
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return result
    }

该函数是将任意(只要安卓系统支持)的图片格式转为JPG,在出现WEBP的时候很好用(我改主意了,我觉得转成PNG更好x

@TheSnowfield
Copy link
Contributor

转换格式个人感觉没什么必要, 直接加上webp发送就行.
本来tx就没怎么用webp吧, 能发但是部分客户端支援不佳也属于情理之中.
要么就直接用支援较好的格式, 要么就自己去转换. 🤔

emmmm 个人设计API的时候会考虑预期结果 在上传图片的时候肯定是预期所有客户端都支持的 所以就没有允许另外的格式
或者 问问项目维护者的意见? @Him188 @Karlatemp

如果考虑到兼容性, 那么就建议不要支援webp. 或者做好格式转换填补tx屎坑.

@TheSnowfield
Copy link
Contributor

也可以判断平台,如果是JVM平台,就用JVM自带的图片处理,如果是安卓平台,我已经写好了实现(土掉渣的反射,调用安卓的API)

    fun decodeWebP(encoded: ByteArray): ByteArray? {
        var result:ByteArray? = null
        val bitmapFactoryClazz = Class.forName("android.graphics.BitmapFactory")
        val bitmapClazz = Class.forName("android.graphics.Bitmap")
        val compressFormatClazz = Class.forName("android.graphics.Bitmap\$CompressFormat")
        val decodeByteArrayMethod = bitmapFactoryClazz.getMethod("decodeByteArray", ByteArray::class.java, Int::class.java, Int::class.java) // public static Bitmap decodeByteArray(byte[] data, int offset, int length) {
        val compressMethod = bitmapClazz.getMethod("compress", compressFormatClazz, Int::class.java, OutputStream::class.java)
        val recycleMethod = bitmapClazz.getMethod("recycle")
        val bitmap = decodeByteArrayMethod.invoke(null,encoded, 0, encoded.size)//BitmapFactory.decodeByteArray(encoded, 0, encoded.size)
        val byteArrayOutputStream = ByteArrayOutputStream()
        try {
            val bos = BufferedOutputStream(byteArrayOutputStream)
            compressMethod.invoke(bitmap, compressFormatClazz.getField("PNG").get(null), 100, bos)//bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos)
            bos.flush()
            result = byteArrayOutputStream.toByteArray()
            bos.close()
            recycleMethod.invoke(bitmap)
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return result
    }

该函数是将任意(只要安卓系统支持)的图片格式转为JPG,在出现WEBP的时候很好用(我改主意了,我觉得转成PNG更好x

webp支援动态图片, 转换成jpg/png必然会丢失帧数据.
apng是最适合的, 但是尚不清楚能否发送apng,以及把apng当作png发送会发生什么事情. (apng向下兼容png

@Karlatemp
Copy link
Member

直接加上webp发送就行

可以添加一个环境参数来忽略预检查错误, 至于最终的显示效果嘛

添加格式转换

可以考虑加个 ImageConverter 来转换不支持的图片到支持的格式, 不过该模块不会直接包含在 core 内, 而是作为独立的可选模块专门维护

PR welcome

@zhaodice
Copy link
Contributor Author

zhaodice commented Sep 26, 2021

直接加上webp发送就行

可以添加一个环境参数来忽略预检查错误, 至于最终的显示效果嘛

添加格式转换

可以考虑加个 ImageConverter 来转换不支持的图片到支持的格式, 不过该模块不会直接包含在 core 内, 而是作为独立的可选模块专门维护

PR welcome

那么这个工具类可以根据环境,判断是安卓还是JVM,因为两种环境都有原生的系统API来处理图片,可以省很多麻烦(当然可以选择由开发者自行实现)

@Him188 Him188 changed the title 发送webp图片时报错:considering use gif/png/bmp/jpg 支持 WEBP 格式图片发送 Sep 29, 2021
@Him188 Him188 added this to the Backlog milestone Sep 29, 2021
@Him188 Him188 added t:feature 类型: 新特性 N 优先级: 一般 and removed x:question 标签: 需要更多信息 labels Sep 29, 2021
@AdorableParker
Copy link

QQ客户端好像本身就不支持使用webp格式的文件(至少你发图片的筛选器里面是没有webp这东西的

@zhaodice
Copy link
Contributor Author

zhaodice commented Oct 2, 2021

我已经把webp转png发送了,目前没有什么问题,

——动态webp图片这么高级的玩意我暂时也用不上。

@zhaodice
Copy link
Contributor Author

zhaodice commented Oct 2, 2021

转换库,检测到非PNG/JPG/GIF则试图转换成PNG(for Android)

package zhao.dice.plugin

import java.io.BufferedOutputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.OutputStream
import java.lang.reflect.Method
import java.util.*

object AndroidImageDecoder {

    private lateinit var bitmapFactoryClazz: Class<*>
    private lateinit var bitmapClazz: Class<*>
    private lateinit var compressFormatClazz: Class<*>
    private lateinit var decodeByteArrayMethod: Method
    private lateinit var compressMethod: Method
    private lateinit var recycleMethod: Method
    private lateinit var compressFormatPNG : Any

    private val formatJPG = hexStringToBytes("FFD8FF")
    private val formatPNG = hexStringToBytes("89504E47")
    private val formatGIF = hexStringToBytes("47494638")

    private val isAndroid = "Dalvik" == System.getProperty("java.vm.name")

    init{
        if(isAndroid) {
            bitmapFactoryClazz = Class.forName("android.graphics.BitmapFactory")
            bitmapClazz = Class.forName("android.graphics.Bitmap")
            compressFormatClazz = Class.forName("android.graphics.Bitmap\$CompressFormat")
            decodeByteArrayMethod = bitmapFactoryClazz.getMethod("decodeByteArray", ByteArray::class.java, Int::class.java, Int::class.java) // public static Bitmap decodeByteArray(byte[] data, int offset, int length) {
            compressMethod = bitmapClazz.getMethod("compress", compressFormatClazz, Int::class.java, OutputStream::class.java)
            recycleMethod = bitmapClazz.getMethod("recycle")
            compressFormatPNG = compressFormatClazz.getField("PNG").get(null)
        }
    }

    fun decode(encoded: ByteArray): ByteArray? {
        if(isAndroid) {
            var result: ByteArray? = null
            val bitmap = decodeByteArrayMethod.invoke(null, encoded, 0, encoded.size)//BitmapFactory.decodeByteArray(encoded, 0, encoded.size)
            val byteArrayOutputStream = ByteArrayOutputStream()
            try {
                val bos = BufferedOutputStream(byteArrayOutputStream)
                compressMethod.invoke(bitmap, compressFormatPNG, 100, bos)//bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos)
                bos.flush()
                result = byteArrayOutputStream.toByteArray()
                bos.close()
                recycleMethod.invoke(bitmap)
            } catch (e: IOException) {
                e.printStackTrace()
            }
            return result
        }else{
            //JVM 平台还未实现
            throw NoSuchMethodError("Decode method on jvm perform was not implemented!")
        }
    }

    private fun hexStringToBytes(hexString: String?): ByteArray? {
        if (hexString == null || hexString == "") {
            return null
        }
        val uUpperCaseHexString = hexString.toUpperCase(Locale.ROOT)
        val length = uUpperCaseHexString.length / 2
        val hexChars = uUpperCaseHexString.toCharArray()
        val d = ByteArray(length)
        for (i in 0 until length) {
            val pos = i * 2
            d[i] = (charToByte(hexChars[pos]).toInt() shl 4 or charToByte(hexChars[pos + 1]).toInt()).toByte()
        }
        return d
    }
    private fun charToByte(c: Char): Byte {
        return "0123456789ABCDEF".indexOf(c).toByte()
    }
    fun isFormatSupported(byte:ByteArray):Boolean{
        return byte.startsWith(formatJPG) || byte.startsWith(formatPNG) || byte.startsWith(formatGIF)
    }
    private fun ByteArray.startsWith(param:ByteArray?):Boolean{
        if(param==null){
            return false
        }
        if(this.size < param.size){
            return false
        }
        for(index in param.indices){
            if(this[index] != param[index]){
                return false
            }
        }
        return true
    }
}

发图部分

val bytes = file.readBytes()
val externalResource = if(AndroidImageDecoder.isFormatSupported(bytes)){
    bytes.toExternalResource()
}else{//Mirai 不支持这种图片格式,所以使用系统API提供的图片解码器
    (AndroidImageDecoder.decode(file.readBytes()))?.toExternalResource() ?: return@async "#图片文件格式错误#"
}.apply {
    Thread{
        Thread.sleep(5000)
        close()
    }.start()
}
return@async externalResource.uploadAsImage(contact).also {
    cacheMediaManager.append(contact,file,it)
}.toString()

@dragon0629
Copy link

感觉不太合适,能见得webp图片很多都是动态,比如抖音什么的。
不能以“我用不上”来设计产品方向……

@zhaodice
Copy link
Contributor Author

zhaodice commented Oct 11, 2021

感觉不太合适,能见得webp图片很多都是动态,比如抖音什么的。 不能以“我用不上”来设计产品方向……

事实上,很多网页都以webp格式来显示图片,特别是图床,因为省流量
不知名的用户直接另存为,发现windows可以正常显示以为是普通图片,就胡乱改成.jpg扩展名,然后就把它当".jpg"发送了...(特别是旧版本mirai可以直接发送webp,更加加重了这是普通图片的错觉)

@zhaodice
Copy link
Contributor Author

事实上webp随着windows的原生支持,地位上已经基本上和jpg、png这类普通格式平起平坐了吧

@AdorableParker
Copy link

感觉不太合适,能见得webp图片很多都是动态,比如抖音什么的。 不能以“我用不上”来设计产品方向……

事实上,很多网页都以webp格式来显示图片,特别是图床,因为省流量 不知名的用户直接另存为,发现windows可以正常显示以为是普通图片,就胡乱改成.jpg扩展名,然后就把它当".jpg"发送了...(特别是旧版本mirai可以直接发送webp,更加加重了这是普通图片的错觉)

强行发送webp格式的图片,在Android端显示是正常的(至少大部分图片都是正常的),PC端的话显示不出来。

事实上webp随着windows的原生支持,地位上已经基本上和jpg、png这类普通格式平起平坐了吧

事实上是不是平起平坐还是得看Tencent那边,人家说可以才算数,qq他不打算支持webp的话,你整出花来也是得转格式,我自己倒是还在用webp格式的,省很多空间

@Itsusinn
Copy link

Itsusinn commented Oct 11, 2021

Work Around on JVM

  implementation("com.github.gotson:webp-imageio:0.2.2") 
 //support linux(arm,amd,etc),mac-os(without arm),win

import com.luciad.imageio.webp.WebPReadParam
import kotlinx.coroutines.runInterruptible
import java.io.File
import javax.imageio.ImageIO
import javax.imageio.stream.FileImageInputStream

@JvmName("ext-isWebp")
suspend fun File.isWebp() = isWebp(this)

suspend fun isWebp(file: File): Boolean = runInterruptible fn@{
  // without setting context classloader, spi won't work. but actually we don't need to set it everytime
  Thread.currentThread().contextClassLoader = Plugin::class.java.classLoader // your mirai-console-plugin
  val iis = ImageIO.createImageInputStream(file)
  val ir = ImageIO.getImageReaders(iis)
  val next = ir.next()
  return@fn when (next.formatName) {
    "WebP" -> true
    else -> false
  }
}
suspend fun convertWebpToPng(from: File, to: File) = runInterruptible {
  Thread.currentThread().contextClassLoader = Plugin::class.java.classLoader
  val reader = ImageIO.getImageReadersByMIMEType("image/webp").next()
  val readParam = WebPReadParam().apply {
    isBypassFiltering = true
  }
  reader.input = FileImageInputStream(from)
  val image = reader.read(0, readParam)
  ImageIO.write(image, "png", to)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
N 优先级: 一般 s:core 子系统: mirai-core t:feature 类型: 新特性
Projects
None yet
Development

No branches or pull requests

9 participants