拒絕卡脖子 | 實(shí)現(xiàn)自己的圖片壓縮工具,想怎么壓就怎么壓
當(dāng)需要壓縮圖片時(shí),到百度上搜索發(fā)現(xiàn)都是廣告,需要下載軟件和收費(fèi),貧窮不允許我這么任性。
好不容易找到一個(gè)在線免費(fèi)壓縮圖片的網(wǎng)站,又有各種各樣的限制,比如我一直在用的在線免費(fèi)壓縮工具【tinypng】,數(shù)量限制無所謂,但是圖片最大5M,往往不能滿足需求,如果需要使用高級功能需要付費(fèi)。
當(dāng)我找UI美眉幫忙把圖片處理小點(diǎn)時(shí),嗯哼~~~,何不自己開發(fā)一個(gè)圖片處理工具。
目前軟件都會上傳圖片,一般都會對用戶上傳的圖片大小進(jìn)行限制,并且上傳之后還要壓縮,這也可以為企業(yè)節(jié)省存儲成本的同時(shí),還可提高上傳速率。
本文就基于SpringBoot結(jié)合thumbnailator實(shí)現(xiàn)圖片壓縮,坑已踩過拿去就用,也可以自己開發(fā)一個(gè)壓縮工具不被卡脖子。
本文大綱
- 基于 thumbnailator 實(shí)現(xiàn)圖片壓縮,添加水印,旋轉(zhuǎn)等常用功能
- 踩坑壓縮圖片反而變大的現(xiàn)象,并給出解決方案
創(chuàng)建SpringBoot工程
創(chuàng)建SpringBoot工程并引入相關(guān)依賴,根據(jù)Maven倉庫地址搜索最新版本為0.4.19,因0.4.8版本利用率最高,本工程也是用0.4.8版本,你可以自行選擇版本使用。
pom依賴
<!-- springboot-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.4</version>
</dependency>
<!-- 圖片壓縮依賴 -->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>
<!-- 測試模塊 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
壓縮文件工具類
上傳文件時(shí)接口使用MultipartFile接收文件,所以我們壓縮圖片的方法也接收MultipartFile類型數(shù)據(jù),并返回MultipartFile類型的數(shù)據(jù)【可根據(jù)實(shí)際情況調(diào)整參數(shù)類型】。
package com.stt.thumbnailator.util;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* 圖片處理工具類
*/
@Component
@Slf4j
public class ImgUtil {
/**
* 壓縮圖片,不修改尺寸
* 1、接收 file 源文件,并判斷是否為空
* 2、不修改原尺寸,按比例壓縮圖片
* 3、將壓縮后的圖片封裝為 MultipartFile 類型返回
* @param file
* @return
*/
public MultipartFile zipImg(MultipartFile file) {
// 判空,并且大于20kb再壓縮
if(file == null || file.getSize() <= 20 * 1024) {
return file;
}
// 根據(jù)輸入流壓縮
log.info("壓縮前圖片大小===》{}",file.getSize());
// 字節(jié)文件輸出流,保存轉(zhuǎn)換后的圖片數(shù)據(jù)流
ByteArrayOutputStream outputStream = null;
// 通過輸入流轉(zhuǎn)換為 MultipartFile
ByteArrayInputStream inputStream = null;
try {
outputStream = new ByteArrayOutputStream();
Thumbnails.of(file.getInputStream())
.scale(1f) //按比例放大縮小 和size() 必須使用一個(gè) 不然會報(bào)錯(cuò)
.outputQuality(0.5f) //輸出的圖片質(zhì)量 0~1 之間,否則報(bào)錯(cuò)
.toOutputStream(outputStream); //圖片輸出位置
// 將 outputStream 轉(zhuǎn)換為 MultipartFile
byte[] bytes = outputStream.toByteArray();
inputStream = new ByteArrayInputStream(bytes);
// 創(chuàng)建 MockMultipartFile 對象,該類在【spring-test】依賴中
MockMultipartFile outFile = new MockMultipartFile(file.getOriginalFilename(), file.getOriginalFilename(), file.getContentType(), inputStream);
log.info("壓縮后圖片大小===》{}",outFile.getSize());
// 返回圖片
return outFile;
} catch (IOException e) {
log.error("圖片壓縮失敗===》{}",e);
throw new RuntimeException(e);
}finally {
// 關(guān)閉流
try {
outputStream.close();
inputStream.close();
} catch (IOException e) {
}
}
}
}
上傳文件接口
package com.stt.thumbnailator.controller;
import com.stt.thumbnailator.util.ImgUtil;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 上傳文件接口
*/
@RestController
@RequestMapping("upload")
public class UploadController {
private final ImgUtil imgUtil;
public UploadController(ImgUtil imgUtil) {
this.imgUtil = imgUtil;
}
/**
* 上傳圖片,請求方式需要是POST請求
* @param file
*/
@PostMapping("img")
public String uploadImg(MultipartFile file) {
// 上傳圖片
MultipartFile zipImg = imgUtil.zipImg(file);
return "壓縮成功";
}
}
application.yml
修改配置類默認(rèn)文件上傳大小,默認(rèn)1M根本不夠用。
spring:
servlet:
multipart:
# 單個(gè)文件最大大小,默認(rèn)1MB,該值根據(jù)實(shí)際需求調(diào)整
max-file-size: 5MB
# 一次請求文件最大大小,默認(rèn)10MB,該值根據(jù)實(shí)際需求調(diào)整
max-request-size: 20MB
測試
通過apifox快速發(fā)送請求,你也可以使用postman等工具。
發(fā)現(xiàn)控制臺輸出壓縮前和壓縮后文件大小,壓縮將近3倍,由outputQuality方法參數(shù)控制,值越小壓縮越嚴(yán)重,當(dāng)然要保障圖片的清晰度,和產(chǎn)品需求適當(dāng)調(diào)整。
親測在 0.3 時(shí)圖片會出現(xiàn)模糊
壓縮圖片并存儲
圖片一般都會存儲到文件服務(wù)器,這里暫時(shí)將圖片通過IO流存儲到本地,來看一下圖片清晰度。
/**
* 壓縮圖片,不修改尺寸,存儲到本地
* @param file
*/
public void zipImgToLocation(MultipartFile file) {
// 判空,并且大于20kb再壓縮
if(file == null || file.getSize() <= 20 * 1024) {
return;
}
// 字節(jié)文件輸出流,保存轉(zhuǎn)換后的圖片數(shù)據(jù)流
ByteArrayOutputStream outputStream = null;
// 獲取文件名
String originalFilename = file.getOriginalFilename();
// 獲取文件后綴
String fileType = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
// 存儲文件位置和名字,改名字一般都隨機(jī)生成
File localFile = new File("zipImg." + fileType);
try {
outputStream = new ByteArrayOutputStream();
Thumbnails.of(file.getInputStream())
.scale(1f) //按比例放大縮小 和size() 必須使用一個(gè) 不然會報(bào)錯(cuò)
.outputQuality(0.5f) //輸出的圖片質(zhì)量 0~1 之間,否則報(bào)錯(cuò)
.toFile(localFile);
} catch (IOException e) {
log.error("圖片壓縮失敗===》{}",e);
throw new RuntimeException(e);
}finally {
// 關(guān)閉流
try {
outputStream.close();
} catch (IOException e) {
}
}
}
在個(gè)人電腦前看著并沒有差別,從3.68M壓縮到1.05M,這張圖上傳到頭條之后估計(jì)還得被平臺壓縮,總之可以根據(jù)outputQuality參數(shù)調(diào)整壓縮程度,別太模糊就行。
壓縮變大
壓縮也有坑啊,比如下方代碼,如果你將一張jpg的圖片按照原0.5的壓縮比壓縮后通過outputFormat方法轉(zhuǎn)換成png格式,圖片反而會被增大。
/**
* 壓縮圖片,不修改尺寸,修改為png格式
* @param file
*/
public void zipImgToPng(MultipartFile file) {
// 判空,并且大于20kb再壓縮
if(file == null || file.getSize() <= 20 * 1024) {
return;
}
// 根據(jù)輸入流壓縮
log.info("壓縮前圖片大小===》{}",file.getSize());
// 字節(jié)文件輸出流,保存轉(zhuǎn)換后的圖片數(shù)據(jù)流
ByteArrayOutputStream outputStream = null;
try {
outputStream = new ByteArrayOutputStream();
Thumbnails.of(file.getInputStream())
.scale(1f) //按比例放大縮小 和size() 必須使用一個(gè) 不然會報(bào)錯(cuò)
.outputQuality(0.5f) //輸出的圖片質(zhì)量 0~1 之間,否則報(bào)錯(cuò)
.outputFormat("png") // 修改圖片為png格式
.toOutputStream(outputStream);
log.info("壓縮后圖片大小===》{}",outputStream.size());
} catch (IOException e) {
log.error("圖片壓縮失敗===》{}",e);
throw new RuntimeException(e);
}finally {
// 關(guān)閉流
try {
outputStream.close();
} catch (IOException e) {
}
}
}
圖片由3M增大到了20M
相似的問題,不少小伙伴也都遇到了
該問題在2022年12月31號更新的0.4.19版本中也并沒有解決,所以只能自行處理,也就有大佬總結(jié)出以下規(guī)則,Thumbnails.scale效果會導(dǎo)致圖片大小變大。
根據(jù)多次測試得來的結(jié)果:用jpg轉(zhuǎn)成jpg效果最佳
- 當(dāng)圖片為jpg時(shí)不要轉(zhuǎn)換直接壓縮
- 當(dāng)圖片為png時(shí)也可以直接壓縮,不要轉(zhuǎn)換
- 當(dāng)圖片為png時(shí)可以先轉(zhuǎn)換為jpg再壓縮也可以
添加水印
比如有以下水印,添加到任意一張圖片的右上角
工具類
/**
* 添加水印
* file: 原圖
* markFile:水印
*/
public void addWatermark(MultipartFile file,MultipartFile markFile) {
// 判空
if(file == null) {
return;
}
// 原圖文件流
try {
InputStream inputStream = file.getInputStream();
InputStream markFileInputStream = markFile.getInputStream();
// 讀取數(shù)據(jù)
BufferedImage srcImg = ImageIO.read(inputStream);
BufferedImage markImg = ImageIO.read(markFileInputStream);
//原圖的寬高
int srcWidth = srcImg.getWidth(null);
int srcHeight = srcImg.getHeight(null);
//水印圖片的寬高
int markWidth = markImg.getWidth(null);
int markHeight = markImg.getHeight(null);
//計(jì)算輸出水印圖片的位置x和y軸
int mark_x = srcWidth - srcWidth / 9;
int mark_y = srcWidth / 9-srcWidth / 10;
//計(jì)算輸出水印圖片的大小
int mark_width = srcWidth / 10;
int mark_height = (srcWidth * markHeight) / (10 * markWidth);
//將水印圖片壓縮成輸出的大小
markImg = Thumbnails.of(markImg).size(mark_width,mark_height).asBufferedImage();
String originalFilename = file.getOriginalFilename();
// 獲取文件后綴
String fileType = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
File finalFile = new File("finalFile." + fileType);
//watermark(位置,水印圖,透明度0.5f=50%透明度)
//outputQuality(控制圖片的質(zhì)量,1f=100%高質(zhì)量)
Thumbnails.of(srcImg)
.size(srcWidth, srcHeight)
.watermark(new Coordinate(mark_x,mark_y), markImg, 1f)
.outputQuality(1f)
.toFile(finalFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
接口需要接收兩張圖片,你也盡可以使用多文件上傳
@PostMapping("img/addWatermark")
public String addWatermark(@RequestParam("srcFile") MultipartFile srcFile,@RequestParam("markFile") MultipartFile markFile) {
// 上傳圖片
imgUtil.addWatermark(srcFile,markFile);
return "添加水印成功";
}
測試添加水印,發(fā)送請求時(shí)參數(shù)名一定要和接口中定義的@RequestParam 注解值相同
測試后發(fā)現(xiàn)右上角確實(shí)添加上了水印
圖片旋轉(zhuǎn)
可以通過 rotate 方法在順時(shí)針和逆時(shí)針方向旋轉(zhuǎn)圖片
/**
* 旋轉(zhuǎn)圖片
*/
public void rotate(MultipartFile file) {
try {
String originalFilename = file.getOriginalFilename();
// 獲取文件后綴
String fileType = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
File finalFile = new File("finalFile." + fileType);
Thumbnails.of(file.getInputStream())
.rotate(90) // 角度,正數(shù):順時(shí)針,負(fù)數(shù):逆時(shí)針
.toFile(finalFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
總結(jié)
- 在壓縮,旋轉(zhuǎn),添加水印,甚至裁剪時(shí)可以通過thumbnailator實(shí)現(xiàn)
- 處理后的圖片可以根據(jù)需求進(jìn)行存儲,也可以直接返回給客戶端
- 處理后的數(shù)據(jù)可以是一個(gè)File文件,也可以是一個(gè)數(shù)據(jù)流,根據(jù)不同需求而定
- 你可以在此基礎(chǔ)上開發(fā)自己的圖片處理工具給朋友或企業(yè)使用,豈不非常哇塞!
這就是我在使用的圖片處理技術(shù),當(dāng)然也有一些其他的方案,與其尋找各種方案哪種最優(yōu),不如挑選一種使用,進(jìn)而自行優(yōu)化,選擇技術(shù)的路上往往會浪費(fèi)許多時(shí)間,你覺得呢?