智能协同云图库项目优化(使用图片工具库处理上传缩略图和预览图)

功能说明

需求

缩略图和预览图处理上传(使用图片处理工具库)

文件上传的时候不仅仅上传原图,同时还上传一个缩略图预览图

  • 缩略图用于主页图片列表的展示;
  • 预览图用于图片详情页面展示(预览图比压缩图清晰一些);
  • 同时还保存了原图,用户可以使用 原图下载 保存高清的原图。

技术选型

两种方法实现:

  1. 使用了腾讯云的对象存储(COS),使用数据万象服务的图片处理能力。
  2. 使用一些图片处理的库手动实现。

第三方图片处理库

经过长达数分钟的市场调研(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个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
DylanX
作者分享
学习打卡 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
下载 APP