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);
}
}