智能协同云图库项目优化(使用图片工具库处理上传缩略图和预览图)
功能说明
需求
缩略图和预览图处理上传(使用图片处理工具库)
文件上传的时候不仅仅上传原图,同时还上传一个缩略图和预览图。
- 缩略图用于主页图片列表的展示;
- 预览图用于图片详情页面展示(预览图比压缩图清晰一些);
- 同时还保存了原图,用户可以使用 原图下载 保存高清的原图。
技术选型
两种方法实现:
- 使用了腾讯云的对象存储(COS),使用数据万象服务的图片处理能力。
- 使用一些图片处理的库手动实现。
第三方图片处理库
经过长达数分钟的市场调研(ChatGPT沟通)了解到一些比较优秀的图片处理类库如下:
1. Thumbnailator
2. Imgscalr
3. Apache Commons Imaging
4. ImageIO (Java自带库)
5. Metadata Extractor
6. ImageMagick (via Java Wrapper)
技术确定
- 使用 Thumbnailator 库来做图片的压缩
- 使用 webp-imageio 库来压缩转换 webp 格式(因为Thumbnailator不支持转换 webp,好像是,我尝试的时候报错)
关于 Thumbnailator:
Thumbnailator是Google开源的优秀图片处理的高质量缩略图的第三方Java类库。
官方帮助文档:https://github.com/coobird/thumbnailator/wiki/Examples
官方Github地址:https://github.com/coobird/thumbnailator
maven仓库地址:https://mvnrepository.com/artifact/net.coobird/thumbnailator
依赖安装
- Thumbnailator
▼xml复制代码<!-- https://mvnrepository.com/artifact/net.coobird/thumbnailator --> <dependency> <groupId>net.coobird</groupId> <artifactId>thumbnailator</artifactId> <version>0.4.20</version> </dependency>
- webp-imageio
▼xml复制代码<!-- https://mvnrepository.com/artifact/org.sejda.imageio/webp-imageio --> <dependency> <groupId>org.sejda.imageio</groupId> <artifactId>webp-imageio</artifactId> <version>0.1.6</version>
- 测试
▼java复制代码@SpringBootTest @Slf4j public class PictureProcessTest { /** * 生成缩略图 */ @Test public void toThumbnailImage() throws IOException { /* * size(width,height) 若图片横比256小,高比256小,不变 */ // jpg 格式 Thumbnails.of("/static/test.jpg") .size(256, 256) .toFile("/static/toThumbnailImage.jpg"); // png 格式 Thumbnails.of("/static/test.png") .size(256, 256) .toFile("/static/toThumbnailImage_png.png"); Thumbnails.of("/static/test.webp") .size(256, 256) .toFile("/static/toThumbnailImage_webp.webp"); log.info("图片压缩缩略图成功"); } /** * 生成压缩图(webp)预览图 */ @Test public void toPreviewImage() { // 旧文件地址 // String oldFile = "/static/test.jpg"; // String newFile = "/static/toPreviewImage_jpg.webp"; // String oldFile = "/static/test.png"; // String newFile = "/static/toPreviewImage_png.webp"; String oldFile = "/static/test.webp"; String newFile = "/static/toPreviewImage_webp.webp"; try { // 获取原始文件的编码 BufferedImage image = ImageIO.read(new File(oldFile)); // 创建WebP ImageWriter实例 ImageWriter writer = ImageIO.getImageWritersByMIMEType("image/webp").next(); // 配置编码参数 WebPWriteParam writeParam = new WebPWriteParam(writer.getLocale()); // 设置压缩模式 writeParam.setCompressionMode(WebPWriteParam.MODE_DEFAULT); // 配置ImageWriter输出 writer.setOutput(new FileImageOutputStream(new File(newFile))); // 进行编码,重新生成新图片 writer.write(null, new IIOImage(image, null, null), writeParam); log.info("图片转Webp成功"); } catch (Exception e) { log.error("异常"); } } }
后端开发
数据库字段扩展
数据库表 picture 添加 缩略图字段,预览图字段。
▼sql复制代码ALTER TABLE picture -- 添加新列 ADD COLUMN thumbnailUrl varchar(512) NULL COMMENT '缩略图 url', ADD COLUMN previewUrl varchar(512) NULL COMMENT '预览图 url';
添加的字段会在表最后面,调整一下顺序(强迫症,可不动)
▼sql复制代码alter table picture modify thumbnailUrl varchar(512) null comment '缩略图 url' after url; alter table picture modify previewUrl varchar(512) null comment '预览图 url' after thumbnailUrl;
数据模型添加字段
- PictureMapper.xml 文件
▼xml复制代码<result property="thumbnailUrl" column="thumbnailUrl" jdbcType="VARCHAR"/> <result property="previewUrl" column="previewUrl" jdbcType="VARCHAR"/> <sql id="Base_Column_List"> id,url,thumbnailUrl,previewUrl,name, introduction,category,tags, picSize,picWidth,picHeight, picScale,picFormat,userId, createTime,editTime,updateTime, isDelete </sql>
-
Picture 类
-
PictureVO 类
-
UploadPictureResult 类
▼java复制代码/** * 缩略图 url */ private String thumbnailUrl; /** * 预览图 url */ private String previewUrl;
图片处理工具类
- 写一个图片处理的通用工具类,方便复用
- PictureProcessUtils.java
▼java复制代码@Component @Slf4j public class PictureProcessUtils { /** * 转换缩略图 * - 格式:256 * 256 * - 如果原图宽高小于设定值,不修改 * - (Thumbnails的缩略宽高小于设定值默认好像是不会修改的,为了清晰一点,自己又添加一个判断逻辑) * * @param originalImage 原图 */ public void toThumbnailImage(File originalImage, File thumbnailFile) { try { // 获取原图的宽高 BufferedImage image = ImageIO.read(originalImage); int originalWidth = image.getWidth(); int originalHeight = image.getHeight(); // 设置目标尺寸 int targetWidth = 256; int targetHeight = 256; // 如果目标尺寸大于原图的尺寸,不进行缩放 if (targetWidth > originalWidth || targetHeight > originalHeight) { targetWidth = originalWidth; targetHeight = originalHeight; } // 生成缩略图 Thumbnails.of(originalImage) .size(targetWidth, targetHeight) .toFile(thumbnailFile); } catch (IOException e) { log.error("生成缩略图失败", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "生成缩略图是失败"); } } /** * 转换为压缩图(预览图) * - webp 格式 * * @param originalImage 原图 */ public void toPreviewImage(File originalImage, File previewImage) { try { // 获取原始文件的编码 BufferedImage image = ImageIO.read(originalImage); // 创建WebP ImageWriter实例 ImageWriter writer = ImageIO.getImageWritersByMIMEType("image/webp").next(); // 配置编码参数 WebPWriteParam writeParam = new WebPWriteParam(writer.getLocale()); // 设置压缩模式 writeParam.setCompressionMode(WebPWriteParam.MODE_DEFAULT); // 配置ImageWriter输出 writer.setOutput(new FileImageOutputStream(previewImage)); // 进行编码,重新生成新图片 writer.write(null, new IIOImage(image, null, null), writeParam); } catch (IOException e) { log.error("生成压缩图(预览图)失败", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "生成压缩图(预览图)失败"); } } }
上传功能代码修改
- 图片上传模板:PictureUploadTemplate
▼java复制代码// 注入工具类 @Resource private PictureProcessUtils pictureProcessUtils; /** * 模板方法,定义上传流程 * * @param inputSource 上传文件 或者 文件URL地址 * @param uploadPathPrefix 上传路径前缀 * @return 返回图片上传信息 */ public final UploadPictureResult uploadPicture(Object inputSource, String uploadPathPrefix) { // 1. 校验图片 /* 校验图片(文件或URL地址) */ validPicture(inputSource); // 2. 图片上传地址 String uuid = RandomUtil.randomString(16); /* 获取文件的原始文件名 */ String originFilename = getOriginFilename(inputSource); String originFileSuffix = FileUtil.getSuffix(originFilename); String uploadTime = DateUtil.formatDate(new Date()); // 上传时间 // 原始图片上传图片名称 String uploadFilename = String.format("%s_%s.%s", uploadTime, uuid, originFileSuffix); // 缩略图名称 String thumbnailImageName = String.format("%s_%s_thumbnail.%s", uploadTime, uuid, originFileSuffix); // 压缩图 String previewImageName = String.format("%s_%s_preview.webp", uploadTime, uuid); // 上传路径 String uploadPath = String.format("/%s/%s", uploadPathPrefix, uploadFilename); String thumbnailImagePath = String.format("/%s/%s", uploadPathPrefix, thumbnailImageName); String previewImagePath = String.format("/%s/%s", uploadPathPrefix, previewImageName); File originalFile = null; File thumbnailImage = null; // 缩略图 File previewImage = null; // 压缩图(预览图) try { // 3. 创建临时文件 originalFile = File.createTempFile( "originalImage", "." + originFileSuffix); thumbnailImage = File.createTempFile("thumbnailImage", "." + originFileSuffix); previewImage = File.createTempFile("previewImage", ".webp"); /* 处理文件来源(本地或 URL) */ processFile(inputSource, originalFile); /* 处理图片 */ // 1). 生成缩略图 pictureProcessUtils.toThumbnailImage(originalFile, thumbnailImage); // 2). 生成压缩图(webp) pictureProcessUtils.toPreviewImage(originalFile, previewImage); // 4. 上传图片到对象存储 // 1) 原图 PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, originalFile); ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo(); // 2)缩略图 cosManager.putObject(thumbnailImagePath, thumbnailImage); // 3)压缩图(webp) cosManager.putObject(previewImagePath, previewImage); // 5. 封装返回结果 return buildResult(originFilename, originalFile, uploadPath, imageInfo, thumbnailImagePath, previewImagePath); } catch (Exception e) { log.error("图片上传到对象存储失败", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败"); } finally { // 6. 清理临时文件 deleteTempFile(originalFile, thumbnailImage, previewImage); } } // ... 缩略部分代码 // 封装返回结果,添加两个参数 /** * 封装返回结果 */ private UploadPictureResult buildResult(String originFilename, File file, String uploadPath, ImageInfo imageInfo, String thumbnailImagePath, String previewImagePath) { // ...省略部分代码 uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + uploadPath); uploadPictureResult.setThumbnailUrl(cosClientConfig.getHost() + "/" + thumbnailImagePath); uploadPictureResult.setPreviewUrl(cosClientConfig.getHost() + "/" + previewImagePath); return uploadPictureResult; } // 删除临时文件也需要多删除清理两个临时文件 /** * 删除临时文件 */ public void deleteTempFile(File originalFile, File thumbnailImage, File previewImage) { if (originalFile == null && thumbnailImage == null && previewImage == null) { return; } boolean originalFileDelete = originalFile.delete(); boolean thumbnailImageDelete = thumbnailImage.delete(); boolean previewImageDelete = previewImage.delete(); if (!originalFileDelete || !thumbnailImageDelete || !previewImageDelete) { log.error("file delete error, originalFilePath = {}", originalFile.getAbsolutePath()); } }
注意:我这里的缩略图和压缩图(预览图)上传方法调用的是cosManager.putObject(),因为缩略图和预览图只需要上传,不需要获取到图片的参数信息之类的,cosManager.putPictureObject() 这个是使用数据万象服务获取到图片信息的。后面打算修改这个信息的获取,改为本地直接获取到图片的信息,不使用对象存储提供的。
上传业务代码修改
主要修改调用上传的服务,添加缩略图和预览图的url地址入库
- PictureServiceImpl
▼java复制代码/** * 图片上传 * * @param inputSource 图片文件或Url地址 * @param pictureUploadRequest 图片上传请求 * @param loginUser 上传用户 * @return 上传的图片信息 */ @Override public PictureVO uploadPicture(Object inputSource, PictureUploadRequest pictureUploadRequest, User loginUser) { // ...省略代码 UploadPictureResult uploadPictureResult = pictureUploadTemplate.uploadPicture(inputSource, uploadPathPrefix); // 构造要写入数据库的图片信息 Picture picture = new Picture(); picture.setUrl(uploadPictureResult.getUrl()); picture.setPreviewUrl(uploadPictureResult.getPreviewUrl()); picture.setThumbnailUrl(uploadPictureResult.getThumbnailUrl()); // ...省略代码 }
上传测试
上传了系统支持的四种图片格式(webp、jgp、jpeg、png),压缩效果不错
- webp
- jgp
- jpeg
- png
前端开发
主页面的列表显示改为缩略图
- HomePage.vue
▼html复制代码<img style="height: 180px; object-fit: cover" :alt="picture.name" :src="picture.thumbnailUrl" />
预览页的图片改为预览图
- PictureDetailPage
▼html复制代码<!-- 图片展示区 --> <a-col :sm="24" :md="16" :xl="18"> <a-card title="图片预览"> <a-image style="max-height: 600px; object-fit: contain" :src="picture.previewUrl" /> </a-card> </a-col>
下载功能改为原图下载
- PictureDetailPage
▼vue复制代码<template> <!-- ... 省略代码 --> <!-- 下载 --> <a-button type="primary" @click="doDownload"> 原图下载 <template #icon> <DownloadOutlined /> </template> </a-button> </template> <script setup lang="ts"> // ... 省略代码 /** * 下载 */ const doDownload = () => { downloadImage(picture.value.url) } </script>
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
内容推荐
🛰️ dtSpaceMap - 实时卫星追踪 3D 可视化平台
5
AI应用开发中的流式输出:从协议原理到工程实战的完整指南
38
第1章 LearnWise AI项目介绍
5
智能协同云图库-进入编辑报错解决
4
【入职求助贴】萌新刚入职某大厂做后端开发,目前还在试用期。最近遇到一个棘手的问题,想向大家求助一下。入职不久,leader 给我派了一个任务。跟我说是0.5天就可以解决,我刚毕业入职,做了一个星期没有做出来。实现一个收集定时成功任务的案例。听起来好像不复杂,但我自己摸索着做了一整个星期,到现在还没达到预期效果。这一周我基本是“边学边做”的状态,遇到卡点也会每天主动找 leader 沟通进度和疑问。
2
作者分享
学习打卡 day5:2024/12/19
## 今日完成
1、复习数据挖掘
2、完成云图库项目用户模块功能
3、复习马原
昨天晚上在卸载电脑的Oracle数据库的时候,删除注册表把系统注册表删了,直接系统崩溃🤡。
今天上午整半天也没弄好,后面重置系统,c盘清了所有环境变量,软件都不好用,idea、pycharm等配置全无🤡
真是开心快乐的一天🤡🤡
5
学习打卡 day4:2024/12/17
## 今日完成
1、复习应用统计学
2、完成云图库项目前端初始化
3、个人项目后端管理api开发
4、面试题:java集合十题(集合的看完了,感觉啥也没记住,还需要反复看一下)
5、复习马原
## 未完成
1、算法基础没有看
2、每日两道力扣算法没做(明天必须补上)
## 总结
1、最近算法基本没有刷、后续需要养成每天刷两题的习惯,今天开始记录:21天养成!!!
2、还有打算期末结束之后复习一下java基础的内容,很多东西最近打代码没怎么用,看面试题的时候一些基础知识都没怎么想得起来
3、这一个月结束之后准备开始学习微服务相关的技术课程
4
学习打卡 day3:2024/12/16
✅1、个人项目的管理系统页面部分开发
✅2、复习期末考试:数据挖掘
✅3、面试题十题:java集合
❌4、算法一题,本来打算继续刷一题,晚上打游戏去了,打完宿舍要断电,显示屏没电(找个不想刷了的借口😅😅😅)
前两天放假出去玩了,又两天没有学习,欸~🤡
3
学习打卡 day2:2024/12/12
✅1、完成了智能协同云图库项目的前后端初始化
✅2、开发个人项目,构建管理系统前端:使用Pure Admin模板
✅3、复习算法数据结构基础栈与队列
✅4、面试题十题:java集合
❌5、算法题,今天又一点摆烂了,没有写
最近被期末的事情打乱计划,没怎么学习,算法摆烂三天没做一题了。
明天一定要抽出时间保证刷算法!
7
学习打卡 day1:2024/12/10
加入编程导航一段时间了,现在开始学习打卡,记录自己的学习历程。
✅1.项目开发:开发自己的项目,今天完成了四个模块的后端api开发
✅2.算法数据结构复习:打算从头巩固一下自己的数据结构和算法方面的基础,完成数组,链表,队列的基础知识知识
❌3.算法题:日常两道算法题,今天打代码太久了,没什么时间算算法
✅4.面试题:学习了Java集合方面的五道面试题
❌5.英语:英语不太好,打算先整理记忆一下开发常用的词汇
最近一直在投简历,想找一下实习,但是年底实习也比较难找了,聊了几个公司,也没有着落。
期末了,还有很多课程论文报告之类的,加油吧。
6
