图片处理工具类:基于 Thumbnailator 的便捷解决方案
文章目录
- 前言
- 一、依赖坐标
- 二、工具类:ImageUtil
- 三、测试
- 1.测试配置
- 2. 图片压缩测试
- 3. 比例缩放测试
- 4. 指定尺寸缩放测试
- 5. 图片旋转测试
- 6. 水印添加测试
- 7. 异常处理测试
- 总结
前言
在现代应用开发中,高效、可靠的图片处理能力是提升用户体验的关键环节。本文介绍的图片处理工具类基于 Thumbnailator 和 Java2D 技术栈,提供了从图片压缩、尺寸调整到水印添加的一站式解决方案。通过简洁的API设计和严谨的资源管理,开发者可以轻松实现各种图片处理需求,避免重复造轮子,提高开发效率。
一、依赖坐标
核心依赖
<!--图片处理——thumbnailator--><dependency><groupId>net.coobird</groupId><artifactId>thumbnailator</artifactId><version>0.4.8</version></dependency>
依赖说明
依赖 | 功能 | 版本要求 |
---|---|---|
thumbnailator | 提供图片缩放、旋转等基础功能 | ≥0.4.8 |
Java2D API | 实现水印、画布操作等高级功能 | JDK |
二、工具类:ImageUtil
方法介绍
函数名 | 参数 | 功能 | 技术实现 |
---|---|---|---|
compressImage | file (图片文件), outputFormat (输出格式) | 按默认比例(0.5)压缩图片 | Thumbnails.scale(),自适应JPEG/PNG质量 |
batchCompressImages | files (图片数组), outputFormat (输出格式) | 批量压缩多张图片 | 循环调用compressImage |
resize | file (图片文件), scaleRatio (缩放比例), outputFormat (输出格式) | 按比例缩放(保持宽高比) | Thumbnails.scale(),比例钳制(0.01-10) |
resizeToDimension | file (图片文件), width (目标宽度), height (目标高度), outputFormat (输出格式) | 缩放到指定尺寸(可能变形) | Thumbnails.size() |
rotate | file (图片文件), degrees (旋转角度), outputFormat (输出格式) | 旋转任意角度 | Thumbnails.rotate() |
addTextWatermark | file (图片文件), watermarkText (水印文字), font (字体), color (颜色), position (位置), opacity (透明度), outputFormat (输出格式) | 添加自定义文字水印 | BufferedImage + Graphics2D,抗锯齿渲染 |
完整代码
import net.coobird.thumbnailator.Thumbnails;
import net.coobird.thumbnailator.geometry.Positions;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;/*** 图片处理工具类(压缩、缩放、旋转、加水印)* 基于 Thumbnailator + Java2D,线程安全,资源安全*/
public class ImageUtil {// 默认参数private static final float DEFAULT_SCALE_RATIO = 0.5f;private static final int DEFAULT_JPEG_QUALITY = 85; // 1-100private static final Font DEFAULT_FONT = new Font("微软雅黑", Font.BOLD, 24);private static final Color DEFAULT_COLOR = Color.WHITE;private static final float DEFAULT_OPACITY = 0.8f;/*** 压缩图片(默认比例 0.5,JPEG 质量 85)** @param file 上传文件* @param outputFormat 输出格式:JPEG/PNG* @return 压缩后的字节数组* @throws IOException 处理失败*/public static byte[] compressImage(MultipartFile file, String outputFormat) throws IOException {return resize(file, DEFAULT_SCALE_RATIO, outputFormat);}/*** 批量压缩图片** @param files 文件数组* @param outputFormat 输出格式* @return 每个文件压缩后的字节数组* @throws IOException 任一文件失败则抛出*/public static byte[][] batchCompressImages(MultipartFile[] files, String outputFormat) throws IOException {byte[][] results = new byte[files.length][];for (int i = 0; i < files.length; i++) {results[i] = compressImage(files[i], outputFormat);}return results;}/*** 按比例缩放图片(保持宽高比)** @param file 原图* @param scaleRatio 缩放比例 (0.01 ~ 10]* @param outputFormat 输出格式* @return 缩放后图片数据* @throws IOException 处理失败*/public static byte[] resize(MultipartFile file, float scaleRatio, String outputFormat) throws IOException {validateFile(file);validateScaleRatio(scaleRatio);String format = validateFormat(outputFormat);try (InputStream in = file.getInputStream()) {ByteArrayOutputStream out = new ByteArrayOutputStream();Thumbnails.of(in).scale(clamp(scaleRatio, 0.01f, 10.0f)).outputFormat(format).outputQuality(getQuality(format)).toOutputStream(out);return out.toByteArray();}}/*** 按指定宽高缩放(可能变形)** @param file 原图* @param width 目标宽度 > 0* @param height 目标高度 > 0* @param outputFormat 输出格式* @return 缩放后数据* @throws IOException 处理失败*/public static byte[] resizeToDimension(MultipartFile file, int width, int height, String outputFormat) throws IOException {validateFile(file);if (width <= 0 || height <= 0) {throw new IllegalArgumentException("宽高必须大于 0");}String format = validateFormat(outputFormat);try (InputStream in = file.getInputStream()) {ByteArrayOutputStream out = new ByteArrayOutputStream();Thumbnails.of(in).size(width, height).outputFormat(format).outputQuality(getQuality(format)).toOutputStream(out);return out.toByteArray();}}/*** 旋转图片** @param file 原图* @param degrees 角度:90, 180, 270(支持负数)* @param outputFormat 输出格式* @return 旋转后数据* @throws IOException 处理失败*/public static byte[] rotate(MultipartFile file, int degrees, String outputFormat) throws IOException {validateFile(file);String format = validateFormat(outputFormat);try (InputStream in = file.getInputStream()) {ByteArrayOutputStream out = new ByteArrayOutputStream();Thumbnails.of(in).scale(1.0).rotate(degrees).outputFormat(format).toOutputStream(out);return out.toByteArray();}}/*** 添加文字水印** @param file 原图* @param watermarkText 水印文字* @param font 字体(null 则用默认)* @param color 颜色(null 则白色)* @param position 位置(如 BOTTOM_RIGHT)* @param opacity 透明度 [0.0 ~ 1.0]* @param outputFormat 输出格式* @return 加水印后图片数据* @throws IOException 处理失败*/public static byte[] addTextWatermark(MultipartFile file, String watermarkText, Font font,Color color, Positions position, float opacity,String outputFormat) throws IOException {validateFile(file);if (watermarkText == null || watermarkText.trim().isEmpty()) {throw new IllegalArgumentException("水印文字不能为空");}String format = validateFormat(outputFormat);font = (font != null) ? font : DEFAULT_FONT;color = (color != null) ? color : DEFAULT_COLOR;opacity = clamp(opacity, 0.0f, 1.0f);position = (position != null) ? position : Positions.BOTTOM_RIGHT;BufferedImage originalImage;try (InputStream in = file.getInputStream()) {originalImage = ImageIO.read(in);if (originalImage == null) {throw new IOException("无法识别的图片格式,请上传有效的图片文件。");}}BufferedImage watermarked = new BufferedImage(originalImage.getWidth(), originalImage.getHeight(), BufferedImage.TYPE_INT_ARGB);Graphics2D g2d = watermarked.createGraphics();g2d.drawImage(originalImage, 0, 0, null);// 抗锯齿g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);g2d.setFont(font);g2d.setColor(color);g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));FontMetrics fm = g2d.getFontMetrics();int tw = fm.stringWidth(watermarkText);int th = fm.getAscent();int x = 0, y = 0;switch (position) {case BOTTOM_RIGHT:x = originalImage.getWidth() - tw - 10;y = originalImage.getHeight() - 10;break;case CENTER:x = (originalImage.getWidth() - tw) / 2;y = (originalImage.getHeight() + th) / 2;break;case TOP_LEFT:x = 10;y = th + 10;break;case TOP_RIGHT:x = originalImage.getWidth() - tw - 10;y = th + 10;break;case BOTTOM_LEFT:x = 10;y = originalImage.getHeight() - 10;break;default:x = 10;y = th + 10;}g2d.drawString(watermarkText, x, y);g2d.dispose();ByteArrayOutputStream out = new ByteArrayOutputStream();boolean success = ImageIO.write(watermarked, format, out);if (!success) {throw new IOException("图片写入失败,不支持的格式: " + format);}return out.toByteArray();}private static void validateFile(MultipartFile file) throws IOException {if (file == null || file.isEmpty()) {throw new IllegalArgumentException("图片文件不能为空");}if (file.getSize() > 10 * 1024 * 1024) { // 10MB 限制throw new IOException("图片大小不能超过 10MB");}}private static String validateFormat(String format) {if (format == null) format = "JPEG";format = format.trim().toUpperCase();if (!"JPEG".equals(format) && !"JPG".equals(format) && !"PNG".equals(format)) {throw new IllegalArgumentException("仅支持 JPEG 和 PNG 格式");}return "JPEG".equals(format) || "JPG".equals(format) ? "JPEG" : "PNG";}private static double getQuality(String format) {return "JPEG".equalsIgnoreCase(format) ? DEFAULT_JPEG_QUALITY / 100.0 : 1.0;}private static float clamp(float value, float min, float max) {return Math.max(min, Math.min(max, value));}private static void validateScaleRatio(float scaleRatio) {if (scaleRatio <= 0 || scaleRatio > 10) {throw new IllegalArgumentException("缩放比例应在 (0, 10] 范围内");}}
}
三、测试
测试策略 | 验证点 | 预期结果 |
---|---|---|
压缩功能 | 输出非空,尺寸减小 | 生成有效图片,大小缩减50% |
比例缩放 | 宽高比保持不变 | 新尺寸 = 原尺寸 * scaleRatio |
指定尺寸 | 精确匹配目标尺寸 | 输出图片宽高 = 指定值 |
90°旋转 | 宽高互换 | width_新 = height_原 |
水印添加 | 文字位置正确 | 右下角偏移(10,10)像素 |
空文件 | 异常处理 | 抛出 IllegalArgumentException |
1.测试配置
-
测试框架:JUnit 5
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId> </dependency>
-
测试图片:943×1036JPEG (169.25KB)
-
模拟文件:MockMultipartFile
2. 图片压缩测试
@Test
void testCompressImage() throws IOException {byte[] result = ImageUtil.compressImage(testImage, "JPEG");assertNotNull(result);assertTrue(result.length > 0);System.out.println("压缩后大小: " + result.length / 1024 + " KB");
}
3. 比例缩放测试
@Test
void testResize() throws IOException {byte[] result = ImageUtil.resize(testImage, 0.3f, "JPEG");BufferedImage img = ImageIO.read(new ByteArrayInputStream(result));assertNotNull(img);System.out.println("缩放后尺寸: " + img.getWidth() + "x" + img.getHeight());
}
4. 指定尺寸缩放测试
@Test
void testResizeToDimension() throws IOException {byte[] result = ImageUtil.resizeToDimension(testImage, 200, 200, "PNG");BufferedImage img = ImageIO.read(new ByteArrayInputStream(result));// 计算基于原图宽高比的目标尺寸double aspectRatio = 943.0 / 1036.0;int expectedWidth = (int) Math.round(200 * aspectRatio);int expectedHeight = 200;assertEquals(expectedWidth, img.getWidth(), "宽度应符合预期");assertEquals(expectedHeight, img.getHeight(), "高度应符合预期");
}
5. 图片旋转测试
@Test
void testRotate() throws IOException {byte[] result = ImageUtil.rotate(testImage, 90, "JPEG");BufferedImage img = ImageIO.read(new ByteArrayInputStream(result));// 注意:旋转后宽高互换assertTrue(img.getWidth() > 0 && img.getHeight() > 0);// 可选:验证旋转90度后宽高互换BufferedImage original = ImageIO.read(new ByteArrayInputStream(testImage.getBytes()));assertEquals(original.getWidth(), img.getHeight(), "旋转90度后高度应等于原宽度");assertEquals(original.getHeight(), img.getWidth(), "旋转90度后宽度应等于原高度");
}
6. 水印添加测试
@Test
void testAddTextWatermark() throws IOException {byte[] result = ImageUtil.addTextWatermark(testImage,"测试水印",new java.awt.Font("宋体", java.awt.Font.BOLD, 30),java.awt.Color.RED,Positions.BOTTOM_RIGHT,0.7f,"PNG");BufferedImage img = ImageIO.read(new ByteArrayInputStream(result));assertNotNull(img);System.out.println("加水印后尺寸: " + img.getWidth() + "x" + img.getHeight());ImageIO.write(img, "png", Files.newOutputStream(Paths.get("src/main/resources/picture/test.png")));
}
7. 异常处理测试
@Test
void testInvalidFile() {MockMultipartFile emptyFile = new MockMultipartFile("file", new byte[0]);assertThrows(IllegalArgumentException.class, () -> {ImageUtil.compressImage(emptyFile, "JPEG");});
}
完整代码
import com.fc.utils.ImageUtil;
import net.coobird.thumbnailator.geometry.Positions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;import static org.junit.jupiter.api.Assertions.*;public class ImageUtilTest {private MockMultipartFile testImage;@BeforeEachvoid setUp() throws IOException {// 读取本地测试图片(提前准备一张 test.jpg)byte[] imageBytes = Files.readAllBytes(Paths.get("src/main/resources/picture/test.png"));testImage = new MockMultipartFile("file","test.jpg","image/jpeg",imageBytes);}@Testvoid testCompressImage() throws IOException {byte[] result = ImageUtil.compressImage(testImage, "JPEG");assertNotNull(result);assertTrue(result.length > 0);System.out.println("压缩后大小: " + result.length / 1024 + " KB");}@Testvoid testResize() throws IOException {byte[] result = ImageUtil.resize(testImage, 0.3f, "JPEG");BufferedImage img = ImageIO.read(new ByteArrayInputStream(result));assertNotNull(img);System.out.println("缩放后尺寸: " + img.getWidth() + "x" + img.getHeight());}@Testvoid testResizeToDimension() throws IOException {byte[] result = ImageUtil.resizeToDimension(testImage, 200, 200, "PNG");BufferedImage img = ImageIO.read(new ByteArrayInputStream(result));// 计算基于原图宽高比的目标尺寸double aspectRatio = 943.0 / 1036.0;int expectedWidth = (int) Math.round(200 * aspectRatio);int expectedHeight = 200;assertEquals(expectedWidth, img.getWidth(), "宽度应符合预期");assertEquals(expectedHeight, img.getHeight(), "高度应符合预期");}@Testvoid testRotate() throws IOException {byte[] result = ImageUtil.rotate(testImage, 90, "JPEG");BufferedImage img = ImageIO.read(new ByteArrayInputStream(result));// 注意:旋转后宽高互换assertTrue(img.getWidth() > 0 && img.getHeight() > 0);// 可选:验证旋转90度后宽高互换BufferedImage original = ImageIO.read(new ByteArrayInputStream(testImage.getBytes()));assertEquals(original.getWidth(), img.getHeight(), "旋转90度后高度应等于原宽度");assertEquals(original.getHeight(), img.getWidth(), "旋转90度后宽度应等于原高度");}@Testvoid testAddTextWatermark() throws IOException {byte[] result = ImageUtil.addTextWatermark(testImage,"测试水印",new java.awt.Font("宋体", java.awt.Font.BOLD, 30),java.awt.Color.RED,Positions.BOTTOM_RIGHT,0.7f,"PNG");BufferedImage img = ImageIO.read(new ByteArrayInputStream(result));assertNotNull(img);System.out.println("加水印后尺寸: " + img.getWidth() + "x" + img.getHeight());ImageIO.write(img, "png", Files.newOutputStream(Paths.get("src/main/resources/picture/test.png")));}@Testvoid testInvalidFile() {MockMultipartFile emptyFile = new MockMultipartFile("file", new byte[0]);assertThrows(IllegalArgumentException.class, () -> {ImageUtil.compressImage(emptyFile, "JPEG");});}
}
总结
本图片处理工具类旨在为开发者提供一个简洁、实用的图片处理解决方案。它基于成熟的Thumbnailator和Java2D技术栈,尝试覆盖常见的图片处理需求,包括基本压缩、尺寸调整、旋转和水印添加等功能。通过合理的参数校验和资源管理设计,努力确保工具的稳定性和易用性。
在常见的图片处理场景中,如用户头像上传、产品图展示等,该工具类可能提供一定的便利性。其接口设计尽量保持简洁,希望能降低开发者的使用门槛。当然,实际应用效果可能因具体业务场景而异,建议开发者根据项目需求进行必要的测试和调整。