浅谈Thumbnails压缩gif图片质量的实现方式

Thumbnails是一个比较大众的图片处理工具,类似的工具还有hutool,可以对图片进行裁剪、缩放、旋转、格式转换、水印等。然而它只提供单张图片的压缩,对于gif的压缩,却是需要我们自己去处理。
下面是一段压缩图片质量的代码:
public static BufferedImage compressPic(BufferedImage frame,float scale) throws Exception{
    return Thumbnails.of(frame).outputFormat("jpg").scale(scale).outputQuality(scale).asBufferedImage();
}

其中scale是对图片进行缩放的比例,传入一个0到1之间的浮点数,大于1表示放大,outputQuality代表输出图片的质量,值在0到1之间,但是我在实际压缩图片的使用中,outputQuality这个参数并不生效,图片最终的质量只受到scale影响,不清楚是何原因,可能是因为这个outputQuality取值和图片字节大小有联系。

官方提供了一个根据文件大小动态调整压缩率的计算方式,如下:

    /**
     * 自动调节精度(经验数值)
     *
     * @param size 源图片大小
     * @return 图片压缩质量比
     */
    private static double getAccuracy(long size) {
        double accuracy;
        if (size < 900) {
            accuracy = 0.85;
        } else if (size < 2047) {
            accuracy = 0.6;
        } else if (size < 3275) {
            accuracy = 0.44;
        } else {
            accuracy = 0.4;
        }
        return accuracy;
    }

通过动态调整压缩比例,重复几次压缩,可以实现将单张图片压缩到指定大小一下,代码如下:

    /**
     * 根据指定大小压缩图片
     *
     * @param imageBytes  源图片字节数组
     * @param desFileSize 指定图片大小,单位kb
     * @return 压缩质量后的图片字节数组
     */
    public static byte[] compressPicForScale(byte[] imageBytes, long desFileSize,String prefix) throws Exception{
        long d = desFileSize * 1024;
        if (imageBytes == null || imageBytes.length <= 0 || imageBytes.length <= d) {
            return imageBytes;
        }
        long srcSize = imageBytes.length;
        double accuracy = getAccuracy(srcSize / 1024);

        while (imageBytes.length > d) {
            ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes);
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream(imageBytes.length);
            BufferedImage read = ImageIO.read(inputStream);
            Thumbnails.of(read)
                    .scale(accuracy)
                    .outputQuality(accuracy)
                    .outputFormat(prefix)
                    .toOutputStream(outputStream);
            imageBytes = outputStream.toByteArray();
        }
        return imageBytes;
    }

这种方式在压缩单张图片时没有问题,然而压缩gif时并不适用,因为gif是由多张图片组成的,每一张都是GIF的一个帧,每帧之间有一个间隔播放时间,类似于视频,所以我们要压缩gif,有两个方向去实现:

1.压缩每张图片的质量

        我们可以读取到gif的每一帧,然后对每一帧都进行压缩,最后再将压缩后的帧按顺序拼接起来,组成新的gif,原则上新的gif占用的空间会减少。

        然而存在一个问题,我们使用上面的代码去压缩时,由于每一帧图片大小不一致,压缩的比例也不一样,无法事先确定图片宽高,导致压缩出来的每帧的宽高都不一致,这样就导致gif没法看了。

        因此我们不能将图片帧按大小压缩,而是应该直接进行缩放比例,这样也能压缩质量,保证每张图片宽高一致。

2.减少帧的数量

        一般比较大的图片,都是帧数非常多,通过减少帧数也能显著降低图片大小,可以对帧数进行一个采样,只选出固定数量的帧数进行压缩,然后拼接成新的gif,这样的方式其实我们在日常生活中有时也能看到,gif变成了一帧一帧不连续的动图。

        采样的方式有很多种,顺序采样,或者均匀采样,随机采样等等。


对于缩放比例,我们可以根据图片宽高进行动态计算:

    public static float getScare(int width,int height){
        int n = Math.max(width,height);
        float rate;
        //大于450像素
        if(n >= 450){
            //缩放到450*0.2=90像素
            rate = 0.2f;
        }else if(n >= 400){
            rate = 0.26f;
        }else if(n >= 300){
            rate = 0.3f;
        }else if(n >= 200){
            rate = 0.4f;
        }else if(n >= 100){
            rate = 0.6f;
        }else if(n >= 80){
            rate = 0.7f;
        }else{
            rate = 0.8f;
        }
        return rate;
    }

gif生成使用到了如下依赖:

        <dependency>
            <groupId>com.madgag</groupId>
            <artifactId>animated-gif-lib</artifactId>
            <version>1.4</version>
        </dependency>

具体压缩gif代码如下:

    public static byte[] compressGif(byte[] data) throws Exception{
        ByteArrayInputStream stream = new ByteArrayInputStream(data);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        GifDecoder decoder = new GifDecoder();
        decoder.read(stream);
        int cnt = decoder.getFrameCount();
        //如果只有一帧,直接压缩到20kb
        if(cnt == 1){
            return compressPicForScale(data, 20, "jpg");
        }
        int width = decoder.getImage().getWidth();
        int height = decoder.getImage().getHeight();
        float scale = getScare(width,height);
        width = (int) (width*scale);
        height = (int) (height*scale);

        AnimatedGifEncoder e = new AnimatedGifEncoder();
        // 设置生成图片大小
        e.setSize(width, height);
        //保存到数组
        e.start(out);
        //重复次数 0表示无限重复 默认不重复
        e.setRepeat(0);
        //进行采样
        BufferedImage[] fs = getFrames(decoder);

        for (BufferedImage f : fs) {
            if (fs.length > 1) {
                //设置延迟
                int delay;
                if (fs.length == 5) {
                    delay = 200;
                } else if (fs.length == 4) {
                    delay = 400;
                } else {
                    delay = 1000;
                }
                e.setDelay(delay);
            }
            BufferedImage image = compressPic(f, scale);
            e.addFrame(image);

        }
        e.finish();
        return out.toByteArray();
    }

采样代码:

    private static BufferedImage[] getFrames(GifDecoder decoder){
        int cnt = decoder.getFrameCount();
        //我这里只采了5帧
        int max = 5;
        if(cnt<= max){
            BufferedImage[] r = new BufferedImage[cnt];
            for (int i = 0; i < cnt; i++) {
                r[i] = decoder.getFrame(i);
            }
            return r;
        }else if(cnt < max*2){
            BufferedImage[] r = new BufferedImage[max];
            for (int i = 0; i < max; i++) {
                r[i] = decoder.getFrame(i);
            }
            return r;
        }else{
            BufferedImage[] r = new BufferedImage[max];
            int sec = cnt/max;
            int n = 0;
            for (int i = 0; i < cnt && n< max; i+=sec) {
                r[n] = decoder.getFrame(i);
                n++;
            }
            return r;
        }
    }

调用方式:

    public static void main(String[] args) throws Exception{
        byte[] d = FileUtils.readFileToByteArray(new File("D:\\zhou\\111.gif"));
        byte[] compress = compress(d,"111.gif");
        if(compress.length < d){
            //压缩后体积是减小的,才保存
            FileUtils.writeByteArrayToFile(new File("D:\\zhou\\222.gif"),compress);
        }

    }

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值