需求
用户选择要导出的图片数据-图片名称,打上水印,导出压缩包
方案
用户选择图片数据->前端发起导出请求->后端创建zip数据流->遍历图片->打上水印->添加至压缩包->前端接收二进制数据->完成下载
实现
Controller
@RequestMapping("/export")
// 接收图片数据列表的方式根据请求的Content-Type决定,@RequestBody接收的是application/json
public void export(@RequestBody List<String> imageNameList, HttpServletResponse response) throws IOException {
...Service.export(response, imageNameList);
}
Service
/**
* 添加水印
*
* @param text 水印文字
* @param sourceImageFile 图片
*/
public BufferedImage addTextWatermark(String text, SmbFile sourceImageFile) throws IOException {
BufferedImage sourceImage = ImageIO.read(sourceImageFile.getInputStream());
Graphics2D g2d = (Graphics2D) sourceImage.getGraphics();
// 设置水印透明度 0为完全透明
AlphaComposite alphaChannel = AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.3f);
g2d.setComposite(alphaChannel);
// 设置水印颜色
g2d.setColor(Color.GRAY);
// 设置字体-Arial需确定系统中存在该字体,文字为中文需要更改为中文字体
g2d.setFont(new Font("Arial", Font.BOLD, 128));
// 绘制水印-高度宽度的设置可以利用window画图中图片像素查看大致位置
g2d.drawString(text, sourceImage.getWidth() - 820, 100);
g2d.dispose();
return sourceImage;
}
public void export(HttpServletResponse response, List<String> imageNameList) throws IOException {
// 不存在的图片列表
List<String> nonExistentFiles = new ArrayList<>();
// 共享文件夹需获取权限,本地文件夹不需要
String sharePath = "smb://***//***//";
NtlmPasswordAuthentication auth = new NtlmPasswordAuthentication("", username, password);
// 设置响应头和内容类型
response.setHeader("Content-Disposition", "attachment;");
response.setContentType("application/zip");
//创建ZipOutputStream
ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream());
for (String fileName : imageNameList) {
// 获取图片 --本地文件则用File
SmbFile file = new SmbFile(sharePath + fileName + ".jpg", auth);
if (!file.exists()) { // 不存在则记录并跳过
nonExistentFiles.add(fileName);
continue;
}
// 打上水印,水印内容自定义
String text = "";
BufferedImage image = addTextWatermark(text, file);
// 压缩已经打上水印的图片
ZipEntry zipEntry = new ZipEntry(fileName + ".jpg");
zipOutputStream.putNextEntry(zipEntry);
ImageIO.write(image, "jpg", zipOutputStream);
zipOutputStream.closeEntry();
}
zipOutputStream.close();
response.getOutputStream().close();
// 终端输出不存在的文件名
System.out.println("All images have been processed and compressed.");
System.out.println("Non-existent files: " + nonExistentFiles);
}
JavaScript
/*
写在前言:如果不需要传递列表数据或者列表数据少的情况下 直接使用window.location = exportUrl,没必要使用XMLHttpRequest
这里使用XMLHttpRequest的原因是因为列表数据多的情况下使用window.location会超过url长度限制
*/
...
获取选择的数据 imageNameList
...
// 获取按钮,禁用并改变文字,闲的话这部分可以做个进度条更美观
// 这边是采用JQ改变DOM,Vue的话可以直接改变数据动态渲染视图
var button = $('#按钮id');
button.prop('disabled', true);
button.text('正在导出图片');
var xhr = new XMLHttpRequest();
xhr.open('POST', '接口地址/export', true); // 设置请求方法和URL
xhr.responseType = 'blob'; // 设置响应类型为 blob
xhr.setRequestHeader('Content-Type', 'application/json');
// 定义 onprogress 事件处理程序,动态展示KB,XMLHttpRequest没办法像window.location流式下载,只能等blob数据全部接收完成才会开始下载。
// 该部分可省略,但这样用户看不到进度会焦虑。
xhr.onprogress = function(e) {
// 因为压缩包是动态生成的,因此没办法展示百分比下载,这边动态展示KB
button.text('正在导出图片 ' + Math.floor(e.loaded/1024).toString() + 'KB');
};
// 无论访问成功还是失败都会执行
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
button.prop('disabled', false); // 取消按钮的禁用状态
button.text('图片导出'); // 恢复按钮文字
}
};
// 加载完成
xhr.onload = function (e) {
if (xhr.status === 200) {
// 请求成功,在这里处理响应数据
var blob = xhr.response;
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = `图片${Date.now()}.zip`; // 设置下载的文件名
link.click();
} else {
// 请求失败,在这里处理错误
console.error('请求失败:' + xhr.statusText);
}
};
// 在这里设置请求体数据,将列表参数转换为JSON格式
var requestData = JSON.stringify(imageNameList);
xhr.send(requestData); // 发送请求