Gru110110110 / Biscuit

一款Android 便捷高效图片压缩库,更多自定义,灵活配置,缩放部分逆向微信朋友圈压缩效果推算得来,效果非常接近!

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Biscuit

Biscuit是一个便捷的android 压缩图片库。由于微信是行业标杆,所以在写本库的时候,特意研究了下微信的压缩效果,以在小米NOTE LTE上为例,经过观察微信压缩效果,逆向推算出微信可能的压缩方式,发现微信很大概率上采用缩放压缩方式。于是本库采用两种压缩方式(采样率、缩放)供使用者选择使用,默认是采用和微信类似的缩放压缩方式并且效果非常接近!

功能

  • 可以单张或者批量进行压缩
  • 可以自定义保存路径
  • 可以自定义压缩后是否使用原图名字命名
  • 可以自定义压缩质量范围
  • 可以选择缩放压缩或者采样率压缩
  • 可以自定义执行器
  • 可以自定义是否忽略透明度(忽略则质量差些,大小也将减小一半)
  • OOM catch,避免Crash
  • 可以清除缓存
  • 压缩后拓展名不变
  • 可以控制log输出
  • 可以设置文件大小小于某个阈值的原图不压缩直接返回原图路径
  • 提供同步方法syncCompress,同步压缩并返回压缩后路径,压缩失败返回原路径
  • 提供压缩全部完成监听

压缩效果对比

先一睹为快!左边是微信压缩效果,右边是Biscuit压缩效果:

上图中八张图片压缩数据对比:

原图 Biscuit Wechat
3120*4160/2.96MB 960*1280/61.58KB 960*1280/61.49KB
1080*9594/6.12MB 1019*9054/880.59KB 1019*9048/801.13KB
1080*5712/3.12MB 1080*5712/622.3KB 1080*5712/621.7KB
1080*2904/311KB 1080*2904/202.8KB 1080*2904/213.24KB
1080*1920/805KB 720*1280/122.2KB 720*1280/118.7KB
3120*4160/3.3MB 960*1280/100.56KB 960*1280/99.18KB
3120*4160/3.39MB 960*1280/93.5KB 960*1280/93.87KB
4160*3120/3.28MB 1280*960/72.57KB 1280*960/71.08KB

可以看到压缩后的图片宽高和微信非常一致!!!图片大小也几乎相等!!!

逆推算法推导

上面八张图,我们整理微信压缩后的宽高和原图做一次数据对比。宽高比,压缩比,这里为了方便分析,我们均取比例小于等于1。宽高比即以最小边除于最大边,并保留小数点后两位。同样的,压缩比即压缩后相应边除于原图相应边得到的比例。

原图 Wechat 宽高比 压缩比 序号
3120*4160 960*1280 0.75 0.30 1
1080*9594 1019*9048 0.11 0.94 2
1080*5712 1080*5712 0.19 1.00 3
1080*2904 1080*2904 0.37 1.00 4
1080*1920 720*1280 0.56 0.66 5
3120*4160 960*1280 0.75 0.30 6
3120*4160 960*1280 0.75 0.30 7
4160*3120 1280*960 0.75 0.30 8

(0,1)区间分析上述宽高比数据,我们可以观察到,序号2、3、4宽高比落入(0,0.5)区间,序号5、1、6、7、8则落入(0.5,1)区间。

 0			0.5			1
  \______________________|______________________/   
    2、3、4                5、1、6、7、8     

下面我们先观察同个区间压缩后的数据,我们很容易发现宽高比落在(0.5,1)区间的压缩后的数据均包含1280这个特征数据。而且特征数据对应于原图长边,再观察以及计算,我们很容易发现特征数据等于原图长边乘以压缩比而得到,换句话说,压缩比等于特征数据除于原图长边。由此,我们可以做出初步假设:当宽高比落入(0.5,1)区间时,并且原图长边大于特征数据1280时,压缩比将由原图最长边除于特征数据1280决定。

再看落入(0,0.5)区间的数据,我们发现序号2的图片压缩后宽高有小幅度变化,其他两张并不压缩宽高。对于有压缩的那张,我们先观察宽高比和压缩比之间的联系,为了便于书写,我们以y代表压缩比,x代表宽高比,那么根据数据,我们可以大致得出一个初步的公式:

     y = 1-x/2      公式①

然后我们再套观察出来的公式①回去计算落在(0,1)区间上的数据,发现另外两张不压缩的图片按此公式压缩后的最短边均小于特征值1000,由此我们可以做出个假设:当宽高比落入区间(0,0.5)时,先按公式①计算出压缩比,然后以图片原图最小边乘以计算出来的压缩比得到的值大于特征值1000时,按公式①进行宽高压缩,否则不压缩。

根据以上分析,我们得出初步待完善的压缩比算法:

    // SCALE_REFERENCE_WIDTH = 1280 , LIMITED_WIDTH = 1000
    private float calculateScaleSize(BitmapFactory.Options options) {
         float scale = 1f;
         int width = options.outWidth;
         int height = options.outHeight;
         int max = Math.max(width, height);
         int min = Math.min(width, height);
         float ratio = min / (max * 1f);//宽高比
         if (ratio >= 0.5f) {//落入区间(0.5,1)
             if (max > SCALE_REFERENCE_WIDTH) scale = SCALE_REFERENCE_WIDTH / (max * 1f);
         } else {//落入区间(0,0.5)
             if (min > LIMITED_WIDTH && (1 - (ratio / 2)) * min > LIMITED_WIDTH) {
                 scale = 1 - (ratio / 2);
             }
         }
         return scale;
     }

我们在此基础上去实验更长的长截图(极端情况):

原图 Wechat 宽高比 压缩比 宽高倍数
1080*10884 1004*10110 0.09 0.93 10
1080*18390 775*13192 0.05 0.72 17

我们发现,虽然上表两张图落入区间(0,0.5)但是毕竟极端,宽高相差十倍以上或者说宽高比小于0.1,这个时候套上面我们观察得到的公式就不符合这种极端情况,所以我们得单独处理,正好,上面八张图中的第二张宽高相差在十倍以下,这就给我们提供个假设:当宽高相差大于十倍时,我们考虑另外的计算方式,而小于十倍时,我们仍然套用之前推导出来的公式。下面我们分析大于十倍的时候压缩比怎么来的。为了便于表达,我们以y代表压缩比,以z代表宽高倍数,我们根据数据,大致可以观察出其中的关系:

     y = 1-(z^2/1000)      公式②

根据观察得来的公式②我们反过去验证,再加权一些系数,以及不至于压缩太小我们限制压缩后的最小边不小于640,于是我们修改后的推导算法如下:

    // SCALE_REFERENCE_WIDTH = 1280 , LIMITED_WIDTH = 1000 , MIN_WIDTH = 640
    private float calculateScaleSize(BitmapFactory.Options options) {
        float scale = 1f;
        int width = options.outWidth;
        int height = options.outHeight;
        int max = Math.max(width, height);
        int min = Math.min(width, height);
        float ratio = min / (max * 1f);
        if (ratio >= 0.5f) {
            if (max > SCALE_REFERENCE_WIDTH) scale = SCALE_REFERENCE_WIDTH / (max * 1f);
        } else {
            if ((max / min) < 10) {
                if (min > LIMITED_WIDTH && (1f - (ratio / 2f)) * min > LIMITED_WIDTH) {
                    scale = 1f - (ratio / 2f);
                }
            } else {
                int multiple = max / min;
                int arg = (int) Math.pow(multiple, 2);
                scale = 1f - (arg / LIMITED_WIDTH) + (multiple > 10 ? 0.01f : 0.03f);
                if (min * scale < Utils.MIN_WIDTH) {
                    scale = 1f;
                }
            }
        }
        return scale;
    }

到此,我们逆推宽高压缩基本完成。下面我们去推导质量压缩部分。质量压缩就简单了,一般为了不失真严重,我们建议压缩质量值在(60,90)之间,根据手机像素密度,给出一个质量值。规则是密度越大,压缩数值可以越低一些,反之,则大一些。规则如下:

 static int DEFAULT_QUALITY = 66;
 static int DEFAULT_LOW_QUALITY = 60;
 static int DEFAULT_HEIGHT_QUALITY = 82;
 static int DEFAULT_X_HEIGHT_QUALITY = 88;
 static int DEFAULT_XX_HEIGHT_QUALITY = 94;
 static int getDefaultQuality(Context context) {
        DisplayMetrics dm = new DisplayMetrics();
        ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(dm);
        float density = dm.density;
        if (density > 3f) {
            return DEFAULT_LOW_QUALITY;
        } else if (density > 2.5f && density <= 3f) {
            return DEFAULT_QUALITY;
        } else if (density > 2f && density <= 2.5f) {
            return DEFAULT_HEIGHT_QUALITY;
        } else if (density > 1.5f && density <= 2f) {
            return DEFAULT_X_HEIGHT_QUALITY;
        } else {
            return DEFAULT_XX_HEIGHT_QUALITY;
        }
    }

Usage

Step 1. Add it in your root build.gradle at the end of repositories:

	allprojects {
		repositories {
			...
			maven { url 'https://jitpack.io' }
		}
	}

Step 2. Add the dependency

	dependencies {
	        compile 'com.github.pruas:Biscuit:v1.3.0'
	}

Step 3. Use it wherever you need

                  Biscuit.with(this)
                        .path(photos)
                        .listener(mCompressListener)//压缩监听
                        .build()
                        .asyncCompress();//异步压缩

Or you can customize like this

                Biscuit.with(this)
                        .path(photos) //可以传入一张图片路径,也可以传入一个图片路径列表进行批量压缩
                        .loggingEnabled(true)//是否输出log 默认输出
//                        .quality(50)//质量压缩值(0...100)默认已经非常接近微信,所以没特殊需求可以不用自定义
                        .originalName(true) //使用原图名字来命名压缩后的图片,默认不使用原图名字,随机图片名字
                        .listener(mCompressListener)//压缩监听,每压缩完成一张即回调一次监听
                        .listener(mOnCompressCompletedListener)//压缩完成监听,只有传入的所有图片都压缩结束才回调
                        .targetDir(FileUtils.getImageDir())//自定义压缩保存路径
//                        .executor(executor) //自定义实现执行,注意:必须在子线程中执行 默认使用HandlerThread执行
//                        .ignoreAlpha(true)//忽略alpha通道,对图片没有透明度要求可以这么做,默认不忽略。
//                        .compressType(Biscuit.SAMPLE)//采用采样率压缩方式,默认是使用缩放压缩方式,也就是和微信效果类似。
                        .ignoreLessThan(100)//忽略小于100kb的图片不压缩,返回原图路径
                        .build()
                        .asyncCompress();//异步压缩

rxjava executor:

                    Biscuit.with(this)
                        .path(photos) //可以传入一张图片路径,也可以传入一个图片路径列表
                        .loggingEnabled(true)//是否输出log 默认输出
                        .listener(mCompressListener)//压缩监听
                        .targetDir(FileUtils.getImageDir())//自定义压缩保存路径
                        .executor(new Executor() {
                            @Override
                            public void execute(Runnable compressor) {
                                Observable.just(compressor).doOnNext(new Consumer<Runnable>() {
                                    @Override
                                    public void accept(Runnable runnable) throws Exception {
                                        runnable.run();
                                    }
                                }).subscribeOn(Schedulers.io()).subscribe();
                            }
                        }) //使用rxjava来执行
                        .ignoreLessThan(100)//忽略小于100kb的图片不压缩,返回原图路径
                        .build()
                        .asyncCompress();

rxjava execute:

          Observable.just(photos).map(new Function<ArrayList<String>, ArrayList<String>>() {
                    @Override
                    public ArrayList<String> apply(@NonNull ArrayList<String> strings) throws Exception {
                        return Biscuit.with(MainActivity.this)
                                .path(strings)
                                .targetDir(FileUtils.getImageDir())
                                .ignoreLessThan(100)
                                .build().syncCompress();//同步方法
                    }
                }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<ArrayList<String>>() {
                    @Override
                    public void accept(ArrayList<String> strings) throws Exception {
                        for (String compressedPath : strings) {
                            info.append("compressed success! the image data has been saved at ");
                            info.append(compressedPath);
                            info.append("\n\n");
                        }
                        mTextView.setText(info.toString());
                    }
                });

Clear cache:

Biscuit.clearCache(this);// default

or

Biscuit.clearCache(FileUtils.getImageDir());//when you have set custom dir

说明

本库是在单一手机上测试,小米Note 1080*1920,所以如果你在使用本库过程中遇到什么问题,欢迎给我提Issues 。QQ交流群号:208317912。

About

一款Android 便捷高效图片压缩库,更多自定义,灵活配置,缩放部分逆向微信朋友圈压缩效果推算得来,效果非常接近!


Languages

Language:Java 100.0%