桥下红药

通往财富的捷径,就是找到你的牧场和羊群
  1. 首页
  2. Java
  3. 正文

优雅的判断图片是否完整

2022年8月8日

下载图片的时候因为使用输出流传输数据的,某些时候中断后,本地图片会因为丢失数据出现下半部分图片灰色撕裂,获取其宽高也不能判断图片是否完整。

# 是什么决定文件的格式?

首先肯定不是文件后缀啦,文件本质就是二进制数据,文件头部二进制数据决定了文件格式。

# JPG格式分析

通过 Notepad++ 二进制模式打开JPG文件,文件开始的2个字节是 FF D8  即 SOI(start of image), 结束2个字节是 FF D9  即 EOD(end of image)。

那么通过读取 EOD 是否匹配,即可判断出文件下载是否完整。

 

# 字节读取方案

读取 SOI 基本没有什么可说的,只需要读取前面2个字节。

/**
* 部分思路
*/
fun readSOI(input: InputStream, length: Int = 2): String {
    val bytes = ByteArray(length)
    IO.read(input, bytes, 0, length)
    return Hex.encodeHexString(bytes)
}
// println FF D8
//

## 读取 EOD

1 )  阻塞读取至结束

/**
 * 逐字节读取到末尾,如果遇到大文件比较耗时
 * 适当增加缓冲区大小可加快读取速度
 * 注意需要手动关闭输入流
 * @param input 输入流
 * @param bufferSize 读取缓冲区大小
 */
fun readEOD(input: InputStream, bufferSize: Int): String {
    var transferred = -1
    var buffer = ByteArray(bufferSize)
    var prevTransferred = -1
    var prevBuffer = mutableListOf<Byte>()
    var read: Int
    while (input.read(buffer, 0, bufferSize).also { read = it } >= 0) {
        transferred = read
        if (transferred == 1) {
            //  最后2个字节被拆分 把
            prevBuffer.addAll(buffer.copyOfRange(0, transferred).toList())
            buffer = prevBuffer.toByteArray()
            transferred = prevTransferred + 1
        } else {
            prevBuffer = buffer.copyOf().toMutableList()
            prevTransferred = transferred
        }
    }

    val bf = buffer.copyOfRange(0, transferred).let {
        it.copyOfRange(it.size - 2, it.size)
    }
    return Hex.encodeHexString(bf)
}

Tip:这种方式分段读取 Bytes ,避免了全部读取到内存中,然后对 读取速度 和 内存使用 做了平衡,仅保留一个分段副本。

 

2 ) 已知输入流长度 skip 读取

fun readEODFast(input: InputStream, skip: Long): String {
    val lastBytes = input.apply {
        skip(skip)
    }.readAllBytes().let {
        it.copyOfRange(it.size - 2, it.size)
    }
    return Hex.encodeHexString(lastBytes)
}

Tip:使用 skip 抛弃指定长度的Byte ,比上述方法更优,只是前提需要数据流的长度。

 

3 ) 使用 RandomAccessFile 随机访问 

/**
 * 读取尾部最优方案
 * 前提是需要输入流长度
 */
fun readBigFileEOD(file: File): String {
    if (!file.exists() || file.isDirectory || !file.canRead()) {
        throw Exception("file is error")
    }
    val raf = RandomAccessFile(file, "r")
    raf.seek(file.length() - 2)
    val buffer = ByteArray(2)
    raf.read(buffer)
    raf.close()
    return Hex.encodeHexString(buffer)
}

Tip:这种方案在读取本地文件的情况下是最优解 ,RandomAccessFile 的指针尤其适合超大文件,seek 直接指定位置进行读取,无需从头读取。

 

# 完整的代码

package test

import java.io.File
import java.io.InputStream
import java.io.RandomAccessFile

class ValidIMG {
 
    fun readSOI(input: InputStream, length: Int = 2): String {
        val bytes = ByteArray(length)
        IO.read(input, bytes, 0, length)
        return Hex.encodeHexString(bytes)
    }

    /**
     * 逐字节读取到末尾,如果遇到大文件比较耗时
     * 适当增加缓冲区大小可加快读取速度
     * 注意需要手动关闭输入流
     * @param input 输入流
     * @param bufferSize 读取缓冲区大小
     */
    fun readEOD(input: InputStream, bufferSize: Int): String {
        var transferred = -1
        var buffer = ByteArray(bufferSize)
        var prevTransferred = -1
        var prevBuffer = mutableListOf<Byte>()
        var read: Int
        while (input.read(buffer, 0, bufferSize).also { read = it } >= 0) {
            transferred = read
            if (transferred == 1) {
                //  最后2个字节被拆分
                prevBuffer.addAll(buffer.copyOfRange(0, transferred).toList())
                buffer = prevBuffer.toByteArray()
                transferred = prevTransferred + 1
            } else {
                prevBuffer = buffer.copyOf().toMutableList()
                prevTransferred = transferred
            }
        }

        val bf = buffer.copyOfRange(0, transferred).let {
            it.copyOfRange(it.size - 2, it.size)
        }
        return Hex.encodeHexString(bf)
    }

    fun readEOD(input: InputStream): String {
        return readEOD(input, 100)
    }

    fun readEODFast(input: InputStream, skip: Long): String {
        val lastBytes = input.apply {
            skip(skip)
        }.readAllBytes().let {
            it.copyOfRange(it.size - 2, it.size)
        }
        return Hex.encodeHexString(lastBytes)
    }

    /**
     * 读取尾部最优方案
     * 前提是需要输入流长度
     */
    fun readBigFileEOD(file: File): String {
        if (!file.exists() || file.isDirectory || !file.canRead()) {
            throw Exception("file is error")
        }
        val raf = RandomAccessFile(file, "r")
        raf.seek(file.length() - 2)
        val buffer = ByteArray(2)
        raf.read(buffer)
        raf.close()
        return Hex.encodeHexString(buffer)
    }

    class Hex {

        companion object {
            private val DIGITS_LOWER = charArrayOf(
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
                'e', 'f'
            )

            /**
             * Used to build output as hex.
             */
            private val DIGITS_UPPER = charArrayOf(
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
                'E', 'F'
            )

            fun encodeHexString(data: ByteArray): String {
                return String(encodeHex(data))
            }

            private fun encodeHex(data: ByteArray): CharArray {
                return org.apache.commons.codec.binary.Hex.encodeHex(data, true)
            }

            fun encodeHex(data: ByteArray, toLowerCase: Boolean): CharArray {
                return encodeHex(data, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER)
            }

            private fun encodeHex(data: ByteArray, toDigits: CharArray): CharArray {
                val l = data.size
                val out = CharArray(l shl 1)
                encodeHex(data, 0, data.size, toDigits, out, 0)
                return out
            }

            fun encodeHex(
                data: ByteArray, dataOffset: Int, dataLen: Int,
                toLowerCase: Boolean
            ): CharArray {
                val out = CharArray(dataLen shl 1)
                encodeHex(data, dataOffset, dataLen, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER, out, 0)
                return out
            }

            fun encodeHex(
                data: ByteArray, dataOffset: Int, dataLen: Int,
                toLowerCase: Boolean, out: CharArray, outOffset: Int
            ) {
                encodeHex(
                    data,
                    dataOffset,
                    dataLen,
                    if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER,
                    out,
                    outOffset
                )
            }

            private fun encodeHex(
                data: ByteArray, dataOffset: Int, dataLen: Int, toDigits: CharArray,
                out: CharArray, outOffset: Int
            ) {
                // two characters form the hex value.
                var i = dataOffset
                var j = outOffset
                while (i < dataOffset + dataLen) {
                    out[j++] = toDigits[0xF0 and data[i].toInt() ushr 4]
                    out[j++] = toDigits[0x0F and data[i].toInt()]
                    i++
                }
            }
        }
    }

    class IO {
        companion object {
            fun read(input: InputStream, buffer: ByteArray, offset: Int, length: Int): Int {
                require(length >= 0) { "Length must not be negative: $length" }
                var remaining = length
                while (remaining > 0) {
                    val location = length - remaining
                    val count = input.read(buffer, offset + location, remaining)
                    if (-1 == count) { // EOF
                        break
                    }
                    remaining -= count
                }
                return length - remaining
            }
        }
    }
}

 

 

 

标签: 暂无
最后更新:2022年8月8日
< 上一篇

文章评论

您需要 登录 之后才可以评论

COPYRIGHT © 2022 桥下红药. ALL RIGHTS RESERVED.

皖ICP备15003861号-1