当前位置: 首页 > news >正文

图片处理工具类:基于 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

方法介绍

函数名参数功能技术实现
compressImagefile (图片文件), outputFormat (输出格式)按默认比例(0.5)压缩图片Thumbnails.scale(),自适应JPEG/PNG质量
batchCompressImagesfiles (图片数组), outputFormat (输出格式)批量压缩多张图片循环调用compressImage
resizefile (图片文件), scaleRatio (缩放比例), outputFormat (输出格式)按比例缩放(保持宽高比)Thumbnails.scale(),比例钳制(0.01-10)
resizeToDimensionfile (图片文件), width (目标宽度), height (目标高度), outputFormat (输出格式)缩放到指定尺寸(可能变形)Thumbnails.size()
rotatefile (图片文件), degrees (旋转角度), outputFormat (输出格式)旋转任意角度Thumbnails.rotate()
addTextWatermarkfile (图片文件), 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技术栈,尝试覆盖常见的图片处理需求,包括基本压缩、尺寸调整、旋转和水印添加等功能。通过合理的参数校验和资源管理设计,努力确保工具的稳定性和易用性。
在常见的图片处理场景中,如用户头像上传、产品图展示等,该工具类可能提供一定的便利性。其接口设计尽量保持简洁,希望能降低开发者的使用门槛。当然,实际应用效果可能因具体业务场景而异,建议开发者根据项目需求进行必要的测试和调整。

http://www.dtcms.com/a/315274.html

相关文章:

  • Unsloth 大语言模型微调工具介绍
  • 数据结构:反转链表(reverse the linked list)
  • 机器视觉的产品包装帖纸模切应用
  • 深度学习-卷积神经网络CNN-卷积层
  • JMeter的基本使用教程
  • 嵌入式学习之51单片机——串口(UART)
  • STM32F103C8-定时器入门(9)
  • slwl2.0
  • Azure DevOps — Kubernetes 上的自托管代理 — 第 5 部分
  • 05-Chapter02-Example02
  • 微软WSUS替代方案
  • Redis与本地缓存的协同使用及多级缓存策略
  • 【定位设置】Mac指定经纬度定位
  • Spring--04--2--AOP自定义注解,数据过滤处理
  • Easysearch 集成阿里云与 Ollama Embedding API,构建端到端的语义搜索系统
  • Shell第二次作业——循环部分
  • 【科研绘图系列】R语言绘制解释度条形图的热图
  • 中标喜讯 | 安畅检测再下一城!斩获重庆供水调度测试项目
  • 松鼠 AI 25 Java 开发 一面
  • 【慕伏白】Android Studio 配置国内镜像源
  • Vue3核心语法进阶(Hook)
  • selenium4+python—实现基本自动化测试
  • PostgreSQL——数据类型和运算符
  • MySQL三大日志详解(binlog、undo log、redo log)
  • C语言的指针
  • 拆解格行随身WiFi技术壁垒:Marvell芯片+智能切网引擎,地铁22Mbps速率如何实现?
  • mysql 数据库系统坏了,物理拷贝出数据怎么读取
  • 深入剖析通用目标跟踪:一项综述
  • 关于如何自定义vscode(wsl连接linux)终端路径文件夹文件名字颜色的步骤:
  • 自学嵌入式 day 42 串口通信