Java图片压缩工具
工具类使用场景
- 公司做人脸识别项目时候需要上传学生、家长、教师、访客的正面照图片,但是人脸识别机器有限制只接收200KB-1M的图片,所以必须做图片压缩到指定范围大少。
- APP上传使用产品的评价附件图片,手机直接拍照上传的图片过大,直接存储导致文件服务器存储容量递减,所以要压缩到指定范围大少。
注意事项
- 最大递归压缩深度默认为20,一般情况下JVM支持1000~2000的栈,所以请勿设置过大的递归深度,否则会抛出:stackoverflowerror.
- 质量比与尺寸比默认均为0.85去压缩,压缩出来的图片少于期待范围时候,会自动递增质量比与尺寸比去压缩。
- 图片压缩比较耗时和性能,能异步的话建议异步处理来消峰。本机压缩6M图片到600KB时候耗时3秒。
依赖关系
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.14</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.29</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
核心代码
1.压缩类
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* @author huangrusheng
* @version 1.0
* @date 2021/5/20 15:27
*/
@Slf4j
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public final class ImageCompressUtil {
/**
* @param imageSource 图片源
* @param outputStream 压缩后的图片输出地
* @return
* @throws IOException
*/
public static boolean compress(ImageSource imageSource, OutputStream outputStream) throws IOException{
boolean result = compress(imageSource);
if(result){
outputStream.write(imageSource.getSource());
}
return result;
}
public static boolean compress(ImageSource imageSource) throws IOException {
if(imageSource.getNumberOfCompressions() == 0){
imageSource.setStartTime(System.currentTimeMillis());
imageSource.setOriginalSource(imageSource.getSource());
}
if( imageSource.getSource().length>=imageSource.getMinCompressToByteSize()
&& imageSource.getSource().length <= imageSource.getMaxCompressToByteSize()) {
log.info("成功完成图片压缩,总耗时:{}秒 , 压缩次数:{} , 压缩前大少:{} KB ,压缩后大少:{}KB.",
(System.currentTimeMillis() - imageSource.getStartTime()) / 1000, imageSource.getNumberOfCompressions(),
imageSource.getByteSizeBeforeCompress() / 1024, imageSource.getSource().length / 1024);
return true;
}
if(imageSource.getNumberOfCompressions() > imageSource.getRecursionDepth()){
log.warn("图片过大,导致递归压缩次数超出:{},请尝试上传其他较小的图片.",imageSource.getNumberOfCompressions());
return false;
}
//如果压缩小于期待值减少,增加下质量与尺寸比例
if(imageSource.getSource().length < imageSource.getMinCompressToByteSize()){
log.warn("压缩过度重新压缩,总耗时:{}秒 , 压缩次数:{} , 压缩前大少:{} KB ,压缩后大少:{}KB.",
(System.currentTimeMillis()-imageSource.getStartTime())/1000 ,imageSource.getNumberOfCompressions(),
imageSource.getByteSizeBeforeCompress()/1024, imageSource.getSource().length/1024 );
imageSource.setScale(imageSource.getScale() + imageSource.getIncrementCompressQuality());
imageSource.setQuality(imageSource.getQuality() + imageSource.getIncrementCompressQuality());
imageSource.setSource(imageSource.getOriginalSource());
}
ByteArrayOutputStream byteArrayOutputStream =
new ByteArrayOutputStream(imageSource.getMaxCompressToByteSize());
Thumbnails.of(new ByteArrayInputStream(imageSource.getSource()))
.scale(imageSource.getScale())
.outputQuality(imageSource.getQuality())
.outputFormat(imageSource.getImageFormat())
.toOutputStream(byteArrayOutputStream);
imageSource.setSource(byteArrayOutputStream.toByteArray());
imageSource.increaseNumberOfCompressions();
return compress(imageSource);
}
}
2.压缩参数POJO类
import lombok.Data;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @author huangrusheng
* @version 1.0
* @date 2021/5/20 15:16
*/
@Data
public class ImageSource {
/**
* 需压缩的图片,转为字节
*/
private byte[] source;
/**
* 记录压缩前的图片,转为字节
*/
private byte[] originalSource;
/**
* 当前图片压缩的次数
*/
private int numberOfCompressions = 0;
private long startTime ;
/**
* 未执行压缩前,图片的字节大少
*/
private int byteSizeBeforeCompress ;
/**
* 压缩质量
*/
private double quality = 0.85;
/**
* 压缩尺寸比例
*/
private double scale = 0.85;
private double incrementCompressQuality = 0.05;
/**
* 需压缩到最大容许的字节数,默认为1M
*/
private int maxCompressToByteSize = 1024*1024*1;
/**
* 需压缩到最小容许的字节数,默认为200KB
*/
private long minCompressToByteSize = 1024*200;
/**
* 压缩后的图片格式,默认JPG
*/
private String imageFormat = "jpg";
/**
* 最大递归压缩深度
*/
private int recursionDepth = 20;
public ImageSource(byte[] source){
this.initSource(source);
}
public ImageSource(InputStream inputStream) throws IOException {
this(inputStream,null);
}
public ImageSource(InputStream inputStream,Long inputByteSize ) throws IOException{
ByteArrayOutputStream byteArrayOutputStream
= new ByteArrayOutputStream(inputByteSize == null ? 2 * 1024 * 1024 : inputByteSize.intValue());
int n ;
byte [] buffer = new byte[1024 * 100];
while ( -1 != ( n = inputStream.read(buffer))){
byteArrayOutputStream.write(buffer,0,n);
}
this.initSource(byteArrayOutputStream.toByteArray());
}
private void initSource(byte[] source){
this.source = source;
this.byteSizeBeforeCompress = this.source.length;
}
public void increaseNumberOfCompressions(){
this.numberOfCompressions = this.numberOfCompressions + 1;
}
}
单元测试用例
import cn.hutool.core.io.IoUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.FileInputStream;
import java.io.FileOutputStream;
/**
* @author huangrusheng
* @version 1.0
* @date 2021/5/20 16:05
*/
@RunWith(JUnit4.class)
public class ImageCompressTest {
@Test
public void testCompress() throws Exception{
FileInputStream fileInputStream = null ;
FileOutputStream fileOutputStream = null;
try {
fileInputStream = new FileInputStream("D:\\image_source.jpg");
fileOutputStream = new FileOutputStream("D:\\image_compress.jpg");
ImageSource imageSource = new ImageSource(fileInputStream);
imageSource.setMinCompressToByteSize(300*1024);
imageSource.setMaxCompressToByteSize(800*1024);
imageSource.setImageFormat("jpg");
ImageCompressUtil.compress(imageSource,fileOutputStream);
}finally {
IoUtil.close(fileInputStream);
IoUtil.flush(fileOutputStream);
IoUtil.close(fileOutputStream);
}
}
}
压缩前
压缩后