美文网首页Android开发Android知识Android进阶之旅
ProgressiveJpeg介绍与在Android中的使用

ProgressiveJpeg介绍与在Android中的使用

作者: oceanLong | 来源:发表于2017-04-06 22:49 被阅读342次

    什么是Jpeg

    JPEG/JFIF是万维网(World Wide Web)上最普遍的被用来存储和传输照片的格式。它并不适合于线条绘图(drawing)和其他文字或图标(iconic)的图形,因为它的压缩方法用在这些类型的图形上,得到的结果并不好(PNG和GIF格式通常是用来存储这类的图形;GIF每个像素只有8比特,并不很适合于存储彩色照片,PNG可以无损地存储照片,但是文件太大的缺点让它不太适合在网络上传输)。

    什么是ProgressiveJpeg

    我们在网页中浏览大图时,如果图片够大,网速够慢,我们能够很清晰的看到一个现象。图片是由模糊到清晰慢慢呈现的。这个就是ProgressiveJpeg所展示的渐进式加载。如下图所示:


    ProgressiveJpeg

    如何生成ProgressiveJpeg

    网上有很多PS生成的方法,不过这不是最方便的方案,七牛可以对上传的图片进行直接转化。

    interlace 是否支持渐进显示。取值1支持渐进显示,取值0不支持渐进显示(默认为0)。适用jpg目标格式,网速慢时,图片显示由模糊到清晰。

    示例:

    https://odum9helk.qnssl.com/resource/gogopher.jpg?imageMogr2/thumbnail/300x300/interlace/1
    

    在Android中如何使用ProgressiveJpeg

    目前,在众多的开源图片加载库中,只有Fresco支持了ProgressiveJpeg格式图片的加载。

    详见Fresco文档

    自此,对于ProgressiveJpeg,我们已经能上手了。

    但Fresco有包体积过大的缺点,我们如果为了支持ProgressiveJpeg就受到了Fresco的其他限制。所以,我们从原理上了解一下ProgressiveJpeg格式,尝试写出一个轻量的库。


    ProgressiveJpeg格式

    Jpeg

    ProgressiveJpeg的编码格式非常复杂,但使用渐进式加载,我们并不需要破解它所有的奥秘。因为不论支不支持渐进式加载,一般的解码器(如Android中的BitmapFactory)一定能够解码出最终完整的Jpeg图片。

    那么,为什么它们无法支持渐进式呢。原来一般的解码器解码图片文件时会把整个文件读完再解码,ProgressiveJpeg的图片中,包含了多Scan(包含了一张图片压缩信息)。因此,ProgressiveJpeg中的一部分数据便足以解码出一张完整的、相对模糊的图片。

    了解到这里,我们便能够很容易地想到,其实渐进式加载的奥秘,其实就是在ProgressiveJpeg的数据流中找到合适的点。当我们读到这个点时,这个点之前的数据便可以被解析出一张图片。同时,我们继续读取数据流,找到下一个可解析点,就可以解析出一张更清晰的图片。


    自己解析ProgressiveJpeg

    我们先来看看解析(寻找某个Scan)的过程。
    首先,我创建了一个OutputStream将读到的数据写入其中,方便随时在读到合适的位置时,用它生成一个byte[]渲染成图片。

    这个合适的位置通过上面的图表,其实是EOI或SOS时。
    当寻找到这个点时,我们调用newScanOrImageEndFound();将数据进行包装传到外部。
    我们先来看看寻找EOI或SOS的代码

    
    /**
     * Progressively scans jpeg data and instructs caller when enough data is available to decode
     * a partial image.
     * <p/>
     * <p> This class treats any sequence of bytes starting with 0xFFD8 as a valid jpeg image
     * <p/>
     * <p> Users should call parseMoreData method each time new chunk of data is received. The buffer
     * passed as a parameter should include entire image data received so far.
     */
    public class ProgressiveJpegParser {
    
        /**
         * Initial state of the parser. Next byte read by the parser should be 0xFF.
         */
        private static final int READ_FIRST_JPEG_BYTE = 0;
    
        /**
         * Parser saw only one byte so far (0xFF). Next byte should be second byte of SOI marker
         */
        private static final int READ_SECOND_JPEG_BYTE = 1;
    
        /**
         * Next byte is either entropy coded data or first byte of a marker. First byte of marker
         * cannot appear in entropy coded data, unless it is followed by 0x00 escape byte.
         */
        private static final int READ_MARKER_FIRST_BYTE_OR_ENTROPY_DATA = 2;
    
        /**
         * Last read byte is 0xFF, possible start of marker (possible, because next byte might be
         * "escape byte" or 0xFF again)
         */
        private static final int READ_MARKER_SECOND_BYTE = 3;
    
        /**
         * Last two bytes constitute a marker that indicates start of a segment, the following two bytes
         * denote 16bit size of the segment
         */
        private static final int READ_SIZE_FIRST_BYTE = 4;
    
        /**
         * Last three bytes are marker and first byte of segment size, after reading next byte, bytes
         * constituting remaining part of segment will be skipped
         */
        private static final int READ_SIZE_SECOND_BYTE = 5;
    
        /**
         * Parsed data is not a JPEG file
         */
        private static final int NOT_A_JPEG = 6;
    
        private static final int DIRECTLY_END = 7;
    
        /**
         * The buffer size in bytes to use.
         */
        private static final int BUFFER_SIZE = 32 * 1024;
    
        private int mBufferSize = BUFFER_SIZE;
    
        private int mParserState;
        private int mLastByteRead;
    
    
    
        public interface OnImageDataListener {
            void onImageDataReady(byte[] datas);
        }
    
        private OnImageDataListener mOnImageDataListener;
    
        public ProgressiveJpegParser() {
            mLastByteRead = 0;
            mParserState = READ_FIRST_JPEG_BYTE;
        }
    
        public void setOnImageDataListener(OnImageDataListener listener) {
            mOnImageDataListener = listener;
        }
    
        private ByteArrayOutputStream mBaos ;
    
    
        private void writeToBaos(ByteArrayOutputStream outputStream, int nextByte) {
            outputStream.write(nextByte);
        }
    
        private void writeToBaos(InputStream inputStream, ByteArrayOutputStream outputStream, int length)
                throws IOException {
    
            byte[] buffer;
            int readNum = 0;
            while (length > mBufferSize) {
                buffer = new byte[mBufferSize];
                int perReadNum = 0;
                while (perReadNum < mBufferSize) {
                    perReadNum += inputStream.read(buffer, 0, mBufferSize - perReadNum);
                    readNum += perReadNum;
                }
            }
            buffer = new byte[length - readNum];
            while (readNum < length) {
                readNum += inputStream.read(buffer, 0, length - readNum);
            }
    
    
            outputStream.write(buffer);
    
        }
    
        private boolean writeToBaos(InputStream inputStream, ByteArrayOutputStream outputStream)
                throws IOException {
            final byte[] bytes = new byte[mBufferSize];
            int count;
            while ((count = inputStream.read(bytes, 0, mBufferSize)) != -1) {
                outputStream.write(bytes, 0, count);
    
            }
            return true;
        }
    
    
        /**
         * Parses more data from inputStream.
         *
         * @param inputStream instance of buffered pooled byte buffer input stream
         */
        public boolean doParseMoreData(final InputStream inputStream, ByteArrayOutputStream outputStream) {
            mBaos = outputStream;
            try {
                int nextByte;
                while ((nextByte = inputStream.read()) != -1) {
                    writeToBaos(outputStream, nextByte);
    
                    switch (mParserState) {
                        case READ_FIRST_JPEG_BYTE:
                            if (nextByte == JfifUtil.MARKER_FIRST_BYTE) {
                                mParserState = READ_SECOND_JPEG_BYTE;
                            } else {
                                mParserState = NOT_A_JPEG;
                            }
                            break;
    
                        case READ_SECOND_JPEG_BYTE:
                            if (nextByte == JfifUtil.MARKER_SOI) {
                                mParserState = READ_MARKER_FIRST_BYTE_OR_ENTROPY_DATA;
                            } else {
                                mParserState = NOT_A_JPEG;
                            }
                            break;
    
                        case READ_MARKER_FIRST_BYTE_OR_ENTROPY_DATA:
                            if (nextByte == JfifUtil.MARKER_FIRST_BYTE) {
                                mParserState = READ_MARKER_SECOND_BYTE;
                            }
                            break;
    
                        case READ_MARKER_SECOND_BYTE:
                            if (nextByte == JfifUtil.MARKER_FIRST_BYTE) {
                                mParserState = READ_MARKER_SECOND_BYTE;
                            } else if (nextByte == JfifUtil.MARKER_ESCAPE_BYTE) {
                                mParserState = READ_MARKER_FIRST_BYTE_OR_ENTROPY_DATA;
                            } else {
                                if (nextByte == JfifUtil.MARKER_SOS || nextByte == JfifUtil.MARKER_EOI) {
                                    newScanOrImageEndFound();
                                }
    
                                if (doesMarkerStartSegment(nextByte)) {
                                    mParserState = READ_SIZE_FIRST_BYTE;
                                } else {
                                    mParserState = READ_MARKER_FIRST_BYTE_OR_ENTROPY_DATA;
                                }
                            }
                            break;
    
                        case READ_SIZE_FIRST_BYTE:
                            mParserState = READ_SIZE_SECOND_BYTE;
                            break;
    
                        case READ_SIZE_SECOND_BYTE:
                            final int size = (mLastByteRead << 8) + nextByte;
                            // We need to jump after the end of the segment - skip size-2 next bytes.
                            // We might want to skip more data than is available to read, in which case we will
                            // consume entire data in inputStream and exit this function before entering another
                            // iteration of the loop.
                            final int bytesToSkip = size - 2;
                            // StreamUtil.skip(inputStream, bytesToSkip);
    
                            // Todo by lsy: Save the skip data in Buffer
                            writeToBaos(inputStream, outputStream, bytesToSkip);
                            mParserState = READ_MARKER_FIRST_BYTE_OR_ENTROPY_DATA;
                            break;
    
                        case NOT_A_JPEG:
                            writeToBaos(inputStream, outputStream);
                            break;
                        case DIRECTLY_END:
                            writeToBaos(inputStream, outputStream);
                            break;
                        default:
                            break;
                    }
    
                    mLastByteRead = nextByte;
                }
            } catch (IOException ioe) {
                // does not happen, input stream returned by pooled byte buffer does not throw IOExceptions
            }
            return true;
        }
    
        /**
         * Not every marker is followed by associated segment
         */
        private static boolean doesMarkerStartSegment(int markerSecondByte) {
            if (markerSecondByte == JfifUtil.MARKER_TEM) {
                return false;
            }
    
            if (markerSecondByte >= JfifUtil.MARKER_RST0 && markerSecondByte <= JfifUtil.MARKER_RST7) {
                return false;
            }
    
            return markerSecondByte != JfifUtil.MARKER_EOI && markerSecondByte != JfifUtil.MARKER_SOI;
        }
    
    
    /**
     * Util for obtaining information from JPEG file.
     */
    public class JfifUtil {
    
      /**
       * Definitions of jpeg markers as well as overall description of jpeg file format can be found
       * here: <a href="http://www.w3.org/Graphics/JPEG/itu-t81.pdf">Recommendation T.81</a>
       */
      public static final int MARKER_FIRST_BYTE = 0xFF;
      public static final int MARKER_ESCAPE_BYTE = 0x00;
      public static final int MARKER_SOI = 0xD8;
      public static final int MARKER_TEM = 0x01;
      public static final int MARKER_EOI = 0xD9;
      public static final int MARKER_SOS = 0xDA;
      public static final int MARKER_APP1 = 0xE1;
      public static final int MARKER_SOFn = 0xC0;
      public static final int MARKER_RST0 = 0xD0;
      public static final int MARKER_RST7 = 0xD7;
      public static final int APP1_EXIF_MAGIC = 0x45786966;
    
      private JfifUtil() {
      }
    
    
    }
    

    接近三百行的代码,比较难以阅读。但对照上面的格式说明,细心读一读会发现,我们就是在寻找上面所说的格式,然后在找到格式后,调用newScanOrImageEndFound();
    上面代码中,我们将读到的所有字节都写入了mBaos中。所以,在newScanOrImageEndFound();中我们将mBaos的数据拿出来做处理。

        private void newScanOrImageEndFound() throws IOException {
    
            if (mOnImageDataListener != null) {
                byte[] bytes = mBaos.toByteArray();
                byte[] tailBytes = new byte[] {(byte) JfifUtil.MARKER_FIRST_BYTE, (byte) JfifUtil.MARKER_EOI};
                byte[] finalBytes = new byte[bytes.length ];
                System.arraycopy(bytes , 0 , finalBytes ,0, bytes.length-2);
                System.arraycopy(tailBytes , 0 , finalBytes ,bytes.length-2, tailBytes.length);
                mOnImageDataListener.onImageDataReady(finalBytes);
            }
    
        }
    

    包装数据也非常简单,由于我们发现的数据是以SOS或EOI结尾的,但我们要欺骗BitmapFactory现在给它的就是完整的数据。所以我们将SOS或EOI的结尾一律替换为EOI的结尾。这类似于告诉BitmapFactory当前的byte[]已经是一张完整的图片啦。

    最后,我们在外面调用这个方法。

    mDecodeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    ProgressiveJpegParser parser = new ProgressiveJpegParser();
                    parser.setOnImageDataListener(
                            new ProgressiveJpegParser.OnImageDataListener() {
                                @Override
                                public void onImageDataReady(byte[] datas) {
                                    mBitmap = BitmapFactory.decodeByteArray(datas, 0, datas.length);
                                    try {
                                        Thread.sleep(1000);
                                    } catch (InterruptedException e) {
                                        e.printStackTrace();
                                    }
                                    mUiHandler.sendEmptyMessage(BITMAP_READY);
                                }
    
                            }
                    );
                    AssetManager assetManager = getAssets();
                    InputStream sourceInput = null;
                    try {
                        sourceInput = assetManager.open("jpeg_test.jpg");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                    parser.doParseMoreData(sourceInput, outputStream);
                }
            });
    
    

    将准备好的byte[]送给BitmapFactory去解析,得到Bitmap就可以显示了。

    通过这种方法,我们就可以在Android设备上也展现出渐进式加载的效果。是不是很cooool。

    但是,这个方法因为会不断地产生byte[]其实非常吃内存。在实际使用中,我们可以考虑限制渐进图片的粒度。比如我只显示前两张,就不再寻找过渡图了,这些优化就不在此赘述。

    以上。

    相关文章

      网友评论

        本文标题:ProgressiveJpeg介绍与在Android中的使用

        本文链接:https://www.haomeiwen.com/subject/fgpsottx.html