All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.message.image.ImageDecoder.kt Maven / Gradle / Ivy

/*
 * 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
 */

package net.mamoe.mirai.internal.message.image

import kotlinx.io.core.*
import kotlinx.io.streams.asInput
import net.mamoe.mirai.message.data.ImageType
import net.mamoe.mirai.utils.*
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..0xC3,
    0xC5..0xC7,
    0xC9..0xCB,
    0xCD..0xCF
)

// https://docs.fileformat.com/image/jpeg/
// http://www.vip.sugovica.hu/Sardi/kepnezo/JPEG%20File%20Layout%20and%20Format.htm
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().toIntUnsigned()
        //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 {
            when (type) {
                //SOS Segment, header is ended
                0xDA -> break
                //0x00 (Byte alignment) and 0x01 (TEM)
                in 0x00..0x01 -> continue
                //RST[0-7] no length and content, skip
                in 0xD0..0xD7 -> continue
                //Normal segment, Skipped size=Segment Length - 2 (Length data itself)
                else -> discardExact(readShort().toIntUnsigned() - 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 (${formatName}) for ExternalResource ${this@calculateImageInfo}, " +
                            "considering use gif/png/bmp/jpg format. " +
                            "image header: ${readBytesOf(max = 30).toUHexString()}"
                )
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy