1. 背景简介及目的
这是一个java读取图片流并进行格式转换,图片高质量压缩,ZIP格式转Base64的极简教程。功能需求是在对接农行支付接口时产生的,满足农行二级商户管理接口中上传商户影印件的功能。写这篇博客的目的是分享给大家我在工作中遇到的实际需求,解决问题的思路,以及一些技术探讨。对接银行的接口开发,他们就直接给文档,是不安排技术支持和联调的,先看一眼文档,目前我要解决的是二级商户证件照资料上传
通过查看报文用例,确认SubMerCertFile字段就是文件流对应的字段。字段类型要求是String,凭借经验,也能知道这个是Base64编码格式,银行的接口文档看着第一眼就觉得奇怪,要求的字段基本都是大写开头的,在现代开发框架中,实体类大写开头的话,可能会产生一些Bug,如spring通过@RequestParam、@PathVariable注入时,会导致找不到属性。
2.相关概念介绍:
Base64 是一种用于将二进制数据编码为 ASCII 字符串的编码方式。它广泛应用于需要以文本格式传输或存储二进制数据的场景,如电子邮件、数据传输、图像嵌入等。
2.1主要特点:
编码原理:
- Base64 使用 64 个字符(A-Z、a-z、0-9、+、/)来表示数据。每 3 个字节的二进制数据会被编码为 4 个 ASCII 字符。
- 为了保证输出总是可以被整齐地分割,Base64 会在数据末尾添加一个或两个等号(
=
)作为填充。
可读性:
- Base64 编码的输出是可打印的 ASCII 字符,适合在文本环境中传输,避免了二进制数据可能带来的问题。
应用场景:
- 常用于将图像、音频等文件嵌入 HTML、CSS 文件中,避免额外的 HTTP 请求。
- 在电子邮件中,Base64 常用于编码附件,使其可以安全传输。
编码和解码:
- 编码时,将每 3 个字节转为 4 个字符;解码时则反向操作,将 4 个字符还原为 3 个字节。
3.准备工作
3.1 引入依赖
<!-- 高质量压缩图设置图片尺寸,转换图片格式-->
<thumbnailator.version>0.4.20</thumbnailator.version>
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>${thumbnailator.version}</version>
</dependency>
4.整体思路
在我们框架中,已经存在公共的图片上传接口,主要是接收一个file流,指定一个分类(枚举值)用来将图片存到不同的存储桶(bucket),最后返回一个绝对路径URL地址,前端将这个地址作为我接口需要的path字段,最终存储到我的数据库的是一个相对路径字符串,之所以存相对路径,是因为存储服务器可能会存在迁移的情况,例如我们项目就是从阿里云迁移到移动云。以下是公共上传接口:
/**
* 上传图片
*
* @param multipartFiles
* @return
*/
@ApiOperation(value = "上传图片")
@ApiImplicitParam(name="ify",value = "图片分类:1-商品,2-身份证,3-营业执照,4-app轮播图, 5-发票,6-银行回执单,7-资讯,8-门头照,9-店内照,10-保险单,11-银行卡正面,12-银行卡反面,13-商品主图,14-商品详情图",required = true,dataType = "integer")
@PostMapping("/api/uploadImages")
public Result<List<UploadVO>> uploadImages(@RequestParam("file") MultipartFile[] multipartFiles,@RequestParam Integer ify) throws IOException {
//上传文件操作
//非本文重点,此处省略
}
结合实际的用户交互,以及框架规范等约束,我直接给出我的思路:
tep1:前端指定ify字段值,调用公共上传组件传照片,并拿到接口返回的绝对路径URL字符串
tep2:用户填写完资料,将上一步拿到的url返回值提交给我的接口
tep3:接口解析完请求参数,使用hutool工具包复制即可,仅需处理URL字符串
tep4:我需要通过前端给的绝对路径,转成图片,这里选择调用EOS(移动云的对象存储)下载功能获取图片流(考虑到Http方式读取图片会有延迟问题影响体验感,还是直接用官方API)
tep5:将图片流转换成jpg(农行这个接口文档没说什么格式,但实际上只能是JPG,这是实战得出来结论,而且用户群体基本是经商的老年人,根本不懂什么JPG,顶多就是拍个照片上传,能独立完成这个用户界面操作就属于很厉害的了)
tep6:判断图片内存大小是否超过1.5M,若是,则采用简单的算术来压缩至1.5M以下
tep7:图片转成zip格式
tep8:zip转成base64,之所以这样细分,也是设计中的单一原则,一个方法就处理一件事情,可以做到最大程度复用。
5.具体的代码
5.1农行接口处理请求
不太喜欢用get,set方法来操作字段,因为代码行数非常多,看起来不够简介,对于第三方的接口,我比较倾向于使用各种序列化框架,直接就能通过名称来取值。
5.2通过eos提供的API获取流
参数是一个String类型,表示接收一个路径地址,然后使用API去连接云储存,再序列化成为java对象,接着就是对流的各种操作。
/**
* 将eos对象存储转换成base64字符串
* @param objectName
* @return
* @throws IOException
*/
public String zip2Base64(String objectName) throws IOException {
objectName = removeLeadingSlash(objectName);
// 填写存储桶(Bucket)所在地域对应的 endpoint 和 Region。
String endpoint = cloudEosProperties.getEndpoint();
String region = cloudEosProperties.getRegion();
String accessKey = cloudEosProperties.getAccessKeyId();
String secretKey = cloudEosProperties.getAccessKeySecret();
String bucketName = cloudEosProperties.getBucketName();
// 创建 AmazonS3 实例。
AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(endpoint, region);
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
AmazonS3 client = AmazonS3ClientBuilder.standard()
.withEndpointConfiguration(endpointConfiguration)
.withCredentials(credentialsProvider).build();
// 下载对象
S3Object s3Object = client.getObject(bucketName, objectName);
try (InputStream inputStream = s3Object.getObjectContent()) {
// 压缩图像并返回压缩后的输入流
InputStream compressedStream = compressStream(inputStream, "jpg", 1.0); // 格式为 "jpg",根据需要调整
// 将压缩后的图像数据压缩成 ZIP 文件,并转换为 Base64 编码
ByteArrayOutputStream zipOutputStream = zip(compressedStream, objectName, "jpg");
byte[] zipBytes = zipOutputStream.toByteArray();
return Base64.getEncoder().encodeToString(zipBytes);
} catch (IOException e) {
log.error("处理文件时出错", e);
throw new RuntimeException("处理文件时出错", e);
}
}
5.3将图片转换成ZIP格式
/**
* 将图片转换为zip格式
* @param inputStream
* @param name
* @param format
* @return
* @throws IOException
*/
public static ByteArrayOutputStream zip(InputStream inputStream, String name, String format) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
ZipEntry zipEntry = new ZipEntry(name + "." + format);
zos.putNextEntry(zipEntry);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
zos.write(buffer, 0, bytesRead);
}
zos.closeEntry();
} catch (IOException e) {
throw new RuntimeException("压缩文件时出错", e);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
return baos;
}
5.4图片压缩至1.5M以下
/**
* 压缩图片至1.5兆以下
* @param inputStream
* @param format
* @param scale
* @return
* @throws IOException
*/
public static InputStream compressStream(InputStream inputStream, String format, double scale) throws IOException {
// 读取 InputStream 到字节数组
byte[] imageBytes = ByteStreams.toByteArray(inputStream);
ByteArrayInputStream originalStream = new ByteArrayInputStream(imageBytes);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
// 压缩图像并写入到 ByteArrayOutputStream
Thumbnails.of(originalStream)
.outputQuality(1)
.scale(scale)
.outputFormat(format)
.toOutputStream(baos);
// 检查文件大小是否大于 1.5MB
if (baos.size() > 1.5 * 1024 * 1024) {
baos.reset(); // 清空输出流,重新压缩
originalStream.reset(); // 重置输入流
Thumbnails.of(originalStream)
.outputQuality(0.3)
.scale(scale)
.outputFormat(format)
.toOutputStream(baos);
}
// 返回 ByteArrayInputStream
return new ByteArrayInputStream(baos.toByteArray());
} catch (IOException e) {
throw new RuntimeException("图片压缩或转换格式出错", e);
} finally {
if (inputStream != null) {
inputStream.close();
}
originalStream.close();
}
}
5.5去掉数据库中路径字段的开头字符
/**
* 去掉相对路径的前缀 斜杠(/)
* @param str
* @return
*/
public static String removeLeadingSlash(String str) {
if (str != null && str.startsWith("/")) {
return str.substring(1);
}
return str;
}
5.6裁剪照片为指定像素
/**
* 裁剪照片为指定像素
* @param inputStream
* @param targetWidth
* @param targetHeight
* @return
* @throws IOException
*/
public static InputStream generateMainImage(InputStream inputStream, Integer targetWidth, Integer targetHeight) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int length;
// 将输入流读取到字节数组中
while ((length = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, length);
}
byte[] imageBytes = byteArrayOutputStream.toByteArray();
// 获取图片尺寸
//Pair<Integer, Integer> pair = getImageDimensions(imageBytes);
/*int originalWidth = pair.getKey();
int originalHeight = pair.getValue();*/
// 检查图片尺寸
/*if (originalWidth < 1000 || originalHeight < 1000) {
throw new RuntimeException("请上传宽高同时大于1000像素的照片");
}*/
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
// 如果目标高度为空,则保持等比缩放
if (targetHeight == null) {
Thumbnails.of(new ByteArrayInputStream(imageBytes))
.outputFormat("jpg")
.outputQuality(0.6)
.width(targetWidth)
.toOutputStream(outputStream);
} else {
// 先等比缩放,再进行居中裁剪
Thumbnails.of(new ByteArrayInputStream(imageBytes))
.outputFormat("jpg")
.outputQuality(1)
.size(targetWidth, targetHeight) // 等比缩放
.crop(Positions.CENTER) // 从中心裁剪
.toOutputStream(outputStream);
}
return new ByteArrayInputStream(outputStream.toByteArray());
} catch (IOException e) {
throw new RuntimeException("裁剪图片出错", e);
} finally {
inputStream.close(); // 可选,视情况决定是否关闭流
}
}
5.7获取图片像素
// 获取图片尺寸
public static Pair<Integer, Integer> getImageDimensions(byte[] bytes) throws IOException {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
int width = image.getWidth();
int height = image.getHeight();
return new Pair<>(width, height);
}
5.8进一步封装
/**
* 裁剪照片为指定的宽度,高度保持宽高比缩放
* @param inputStream
* @param targetWidth
* @return
* @throws IOException
*/
public static InputStream generateMainImage(InputStream inputStream, int targetWidth) throws IOException {
return generateMainImage(inputStream,targetWidth,null);
}
6.关于Thumbnailator 库
Thumbnailator 是一个轻量级的 Java 库,专门用于图像处理。它的主要目标是简化图像缩放和其他操作,如裁剪、旋转、合成等。相对于其他常见的图像处理库(如 Java ImageIO
或 Apache Commons Imaging
),Thumbnailator 更加简洁易用,并且具有更好的性能表现,尤其在图像缩放和调整方面。
7.Thumbnailator基本用法介绍
7.1使用 Thumbnailator
缩放图像
public static void main(String[] args) {
try {
// 读取原始图片文件,并生成缩略图
Thumbnails.of(new File("path/to/original/image.jpg"))
.size(200, 200) // 设置缩略图的宽度和高度
.toFile(new File("path/to/output/thumbnail.jpg"));
System.out.println("缩略图生成成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
7.2 按比例缩放图像
Thumbnails.of(new File("path/to/original/image.jpg"))
.size(200, 200)
.keepAspectRatio(true) // 保持原始比例
.toFile(new File("path/to/output/thumbnail.jpg"));
7.3裁剪图像-固定区域裁剪
public static void main(String[] args) {
try {
Thumbnails.of(new File("path/to/original/image.jpg"))
.sourceRegion(100, 100, 300, 300) // 设定裁剪区域,(x, y, width, height)
.size(200, 200) // 之后将裁剪的区域缩放为200x200
.toFile(new File("path/to/output/cropped_image.jpg"));
System.out.println("裁剪图像成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
7.4裁剪图像-中心裁剪
Thumbnails.of(new File("path/to/original/image.jpg"))
.sourceRegion(Positions.CENTER, 200, 200) // 从中心裁剪出200x200区域
.toFile(new File("path/to/output/cropped_center_image.jpg"));
7.5旋转图像
Thumbnails.of(new File("path/to/original/image.jpg"))
.rotate(90) // 顺时针旋转90度
.toFile(new File("path/to/output/rotated_image.jpg"));
7.6综合操作:缩放、裁剪、旋转
Thumbnails.of(new File("path/to/original/image.jpg"))
.size(400, 400) // 缩放图像为400x400
.rotate(45) // 旋转45度
.sourceRegion(Positions.CENTER, 300, 300) // 从中心裁剪出300x300区域
.toFile(new File("path/to/output/final_image.jpg"));
8.总结
本文既是分享,也是巩固所学所用,运营电商项目,随时会更换商品图片,如果不做控制,上传上来的图片会不适应小程序或者APP,图片会糊。多一个工具,多一种解决问题的手段而已。
标签:极简,Java,String,jpg,inputStream,return,图像处理,IOException,new From: https://blog.csdn.net/Ta20220617/article/details/143496254