Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >ProgressiveJpeg介绍与在Android中的使用

ProgressiveJpeg介绍与在Android中的使用

作者头像
Oceanlong
发布于 2018-07-03 05:03:49
发布于 2018-07-03 05:03:49
1.9K00
代码可运行
举报
运行总次数:0
代码可运行

什么是Jpeg

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

什么是ProgressiveJpeg

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

ProgressiveJpeg

如何生成ProgressiveJpeg

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

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

示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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的代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 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的数据拿出来做处理。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    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[]已经是一张完整的图片啦。

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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[]其实非常吃内存。在实际使用中,我们可以考虑限制渐进图片的粒度。比如我只显示前两张,就不再寻找过渡图了,这些优化就不在此赘述。

以上。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017.04.06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JDK源码:IO体系讲解
IO流是Java中很重要的一部分内容,常用的数据传输,文件的上传和下载都和它分不开。
后台技术汇
2024/10/22
1460
JDK源码:IO体系讲解
Android WebView选择图片、发送图片
主要代码来自:http://blog.csdn.net/woshinia/article/details/19030437 有删改
yechaoa
2022/06/10
9090
压缩算法选型(gzip/snappy/lz4)及性能对比
通过Snappy.compress()进行压缩,压缩后的数据没有magic header
用户7255712
2021/11/21
17.1K0
手写一个简单的Server和Client
西瓜籽:“这也太简单了,我知道发起一个HTTP请求和建立一个Socket连接区别不大,所以我要手写一个HttpClient(客户程序)和HTTPServer(HTTP服务器)。”
东边的大西瓜
2022/05/05
2710
手写一个简单的Server和Client
聊聊flink的BlobService
flink-release-1.7.2/flink-runtime/src/main/java/org/apache/flink/runtime/blob/BlobService.java
code4it
2019/02/27
1.5K0
聊聊flink的BlobService
Java 中图片与二进制之间如何相互转换?
注:该方法的入参,base64 格式文件不得有 文件头部标识信息,否则会转换失败。所以这里我们需要自行判断是否包含有头部信息。
跟着飞哥学编程
2022/12/16
1.1K0
android充当server服务器
    在android上跑起来一个web服务器,可供电脑和手机通过http访问。这个需求并不常见,网上资料也不多,找了一会发现了一个不错的框架。github链接地址https://github.com/NanoHttpd/nanohttpd
天涯泪小武
2019/01/17
5.3K1
Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析
所以最终的出口在于最下面的instrument(input,output,string),下面是剩余部分代码:
JavaEdge
2020/05/27
8190
Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析
Android开发笔记(六十三)HTTP访问的通信方式
输入输出流在java中很常用,从文件读写到内存读写到网络通信都会用到。在之前的《Android开发笔记(三十三)文本文件和图片文件的读写》中,我们学习了文件流FileOutputStream和FileInputStream,以及缓存流BufferedOutputStream和BufferedInputStream。这些输入输出流都继承自InputStream和OutputStream,下面是它们的常用方法: InputStream的常用方法 available : 获取输入流的大小 read : 从输入流中读取数据 close : 关闭输入流 OutputStream的常用方法 write : 往输出流写数据 flush : 刷新输出流 close : 关闭输出流 java在进行http访问操作时,发送数据使用OutputStream,接收数据使用InputStream。如果采用HttpURLConnection,InputStream对象可从HttpURLConnection的getInputStream方法获得;如果采用HttpClient,InputStream对象可从HttpEntity的getContent方法获得。下面是http访问时与InputStream有关的加工操作: 1、从InputStream对象中读取字符串。首先把输入流的数据读到字节流ByteArrayOutputStream,然后调用字节流的toByteArray方法得到字节数组,最后调用String的构造函数根据指定编码从字节数组构造返回字符串; 2、从InputStream对象中读取图像。调用BitmapFactory的decodeStream方法即可返回Bitmap图像数据。 3、从InputStream对象中解压gzip压缩数据。引入GZIPInputStream从输入流构造解压流,然后再从解压流中读取数据。
aqi00
2019/01/18
1.2K0
Android【本地Json处理工具类】
代码 public class ConvertUtils { public static final long GB = 1073741824L; public static final long MB = 1048576L; public static final long KB = 1024L; public ConvertUtils() { } public static int toInt(Object obj) {
全栈程序员站长
2021/04/07
5940
Android中自带的加密和解密
在当今社会信息安全越来越重要,其中最为关键的就是传输过程中的安全。这就需要一套安全可靠且有效的加密和解密算法来实现。
林老师带你学编程
2022/11/30
8230
初级篇,利用 Android 搭建一个简易的文字识别APP-印刷体高精度版本
前端学习参考:https://www.runoob.com/w3cnote/android-tutorial-linearlayout.html
HI hero
2020/04/29
4.3K1
初级篇,利用 Android 搭建一个简易的文字识别APP-印刷体高精度版本
java---文件操作
InputStream输入流 是从程序外部将文件内容读入到程序中 InputStream是一个抽象类。
用户10787181
2023/10/17
2680
java---文件操作
最全的android图片加密
在android开发过程中有些时候一些重要的图片,我们不希望用户通过文件管理直接能查看,我们该怎么办呢,当然你可以把图片放在android的内部存储中,data/data/下,但毕竟android root用户一大堆,还是解决不了问题。那么我们就需要对图片进行加密,当然加密的方法有很多种,下面给大家推荐我常用的2中方法,基本上可以解决大部分问题。
全栈程序员站长
2022/08/31
1.4K0
day24-库的使用(2022.2.21)
=============== 2.字体库的使用 ====================
天天Lotay
2022/12/02
9770
day24-库的使用(2022.2.21)
Android性能优化系列之Bitmap图片优化
在Android开发过程中,Bitmap往往会给开发者带来一些困扰,因为对Bitmap操作不慎,就容易造成OOM(Java.lang.OutofMemoryError - 内存溢出),本篇博客,我们将一起探讨Bitmap的性能优化。
老马的编程之旅
2022/06/22
7980
Android性能优化系列之Bitmap图片优化
springboot生成二维码
我们不造轮子,只是轮子的搬运工。(其实最好是造轮子,造比别人好的轮子) 1、 在pom.xml中加入依赖 <!--二维码--> <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.3.3</version> </dependency> 2、添加工具类 import com.google.zxing.BarcodeFormat; import com.g
吟风者
2019/07/25
1.2K0
相关推荐
JDK源码:IO体系讲解
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验