首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >默语带你玩转Spring Boot:图片上传后自动添加酷炫水

默语带你玩转Spring Boot:图片上传后自动添加酷炫水

作者头像
默 语
发布2025-05-12 09:06:36
发布2025-05-12 09:06:36
3370
举报
文章被收录于专栏:JAVAJAVA

摘要: 嘿,各位努力学习的小伙伴们,我是默语!今天我们要攻克一个在实际项目中非常常见的需求:用户上传图片后,系统自动给图片加上预设的水印,比如公司LOGO或者版权文字。这不仅能保护图片版权,还能起到品牌宣传的作用。很多小白同学可能觉得这个功能听起来有点复杂,涉及到文件处理、图像操作等等。别担心!本文将手把手带你使用Spring Boot,结合Java原生的图像处理能力,轻松实现图片上传和动态添加水印的功能。我会把每一步都讲得仔仔(细细)白白(透透),保证你看完就能上手!

默语带你玩转Spring Boot:图片上传后自动添加酷炫水印(超详细小白教程)

引言:

在互联网应用中,图片资源无处不在。无论是社交分享、电商平台商品图,还是企业内部的文档图片,我们经常需要在用户上传图片后进行一些自动化处理。其中,“添加水印”就是一个非常实用的功能。想象一下,你辛辛苦苦拍摄或制作的图片,被别人随意盗用,是不是很郁闷?或者,你想让用户分享出去的图片都带上你的品牌标识,增加曝光度。这时候,自动水印功能就派上大用场了。

Spring Boot以其“约定大于配置”的理念,极大地简化了Java应用的开发。对于文件上传,Spring Boot也提供了非常便捷的支持。而图片水印的添加,我们可以利用Java AWT (Abstract Window Toolkit) 和 Java 2D API 来实现,无需引入过多的第三方库,保持项目的轻量级。

那么,具体怎么做呢?别急,跟着默语的节奏,一步一步来,你会发现原来这么简单!

正文

一、项目环境准备与基本认知

在开始编码之前,我们先确保开发环境和项目依赖都已就绪。

1.1 Maven 依赖 (pom.xml)

我们需要一个基础的Spring Boot Web项目。确保你的pom.xml文件中包含以下核心依赖:

XML

代码语言:javascript
复制
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

*默语解说:*

代码语言:javascript
复制
spring-boot-starter-web:这是我们构建Web应用的基础,处理文件上传请求就靠它了。
代码语言:javascript
复制
spring-boot-devtools:开发时的好帮手,修改代码后能自动重启应用,省去手动重启的麻烦。
代码语言:javascript
复制
lombok:能帮你少写很多样板代码,比如@Getter, @Setter, @ToString等。
代码语言:javascript
复制
spring-boot-starter-thymeleaf:如果你想快速搭建一个前端页面来测试文件上传,Thymeleaf是个不错的选择。当然,你也可以用Postman等工具直接测试API接口。
1.2 application.properties 配置

src/main/resources/application.properties (或 application.yml) 文件中,我们可以配置一下文件上传的相关限制,比如单个文件大小、总请求大小等。

Properties

代码语言:javascript
复制
# 服务器端口
server.port=8080

# Spring MVC 文件上传配置
spring.servlet.multipart.enabled=true
# 单个文件最大值,默认1MB,这里设置为10MB
spring.servlet.multipart.max-file-size=10MB
# 总请求最大值,默认10MB,这里设置为100MB
spring.servlet.multipart.max-request-size=100MB
# 文件大小阈值,超过后会写入临时文件,默认0,表示所有都写入磁盘
# spring.servlet.multipart.file-size-threshold=2MB

# 自定义配置:文件存储路径(请根据你的实际情况修改)
# 注意:生产环境建议使用更健壮的路径管理方式或对象存储服务
file.upload-dir=D:/myapp_uploads/images
# 水印图片路径 (如果使用图片水印)
watermark.image.path=classpath:watermark/logo.png # 假设水印图片在resources/watermark/logo.png

默语解说:

代码语言:javascript
复制
spring.servlet.multipart.enabled=true:确保开启了文件上传支持。
代码语言:javascript
复制
max-file-size 和 max-request-size:这两个参数很重要,可以防止用户上传过大的文件导致服务器资源耗尽。根据你的业务需求调整它们的值。
代码语言:javascript
复制
file.upload-dir:这是我们自定义的一个属性,用来指定上传的文件保存到服务器的哪个位置。

请务必修改为你自己机器上存在的有效路径!

代码语言:javascript
复制
watermark.image.path:如果我们要添加图片水印,这里指定水印图片的路径。

classpath:表示从项目的resources目录下查找。

二、实现文件上传功能

首先,我们需要一个Controller来接收前端上传的文件。

2.1 创建文件上传的Controller

Java

代码语言:javascript
复制
package com.moyu.blog.controller;

import com.moyu.blog.service.FileStorageService;
import com.moyu.blog.service.WatermarkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Path;
import java.nio.file.Paths;

@Controller
@RequestMapping("/files")
public class FileUploadController {

    private final FileStorageService fileStorageService;
    private final WatermarkService watermarkService;

    @Value("${file.upload-dir}")
    private String uploadDir;

    @Autowired
    public FileUploadController(FileStorageService fileStorageService, WatermarkService watermarkService) {
        this.fileStorageService = fileStorageService;
        this.watermarkService = watermarkService;
    }

    // 提供一个简单的上传页面
    @GetMapping("/upload")
    public String showUploadForm(Model model) {
        // 可选: 列出已上传文件
        // model.addAttribute("files", fileStorageService.loadAll().map(
        // path -> MvcUriComponentsBuilder.fromMethodName(FileUploadController.class,
        // "serveFile", path.getFileName().toString()).build().toUri().toString())
        // .collect(Collectors.toList()));
        return "upload-form"; // 返回 src/main/resources/templates/upload-form.html
    }

    @PostMapping("/upload-with-watermark")
    public String handleFileUploadWithWatermark(@RequestParam("file") MultipartFile file,
                                                @RequestParam(value = "watermarkText", required = false, defaultValue = "默语博客") String watermarkText,
                                                @RequestParam(value = "watermarkType", defaultValue = "text") String watermarkType, // "text" or "image"
                                                RedirectAttributes redirectAttributes,
                                                Model model) {
        if (file.isEmpty()) {
            redirectAttributes.addFlashAttribute("message", "请选择一个文件上传!");
            return "redirect:/files/upload";
        }

        try {
            // 1. 保存原始文件 (可选,或者直接处理流)
            // String originalFilename = fileStorageService.store(file);

            // 2. 为添加水印后的文件生成新文件名
            String originalFilename = file.getOriginalFilename();
            String fileExtension = "";
            if (originalFilename != null && originalFilename.contains(".")) {
                fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
            }
            String watermarkedFilename = "watermarked_" + System.currentTimeMillis() + "_" + originalFilename;
            Path watermarkedFilePath = Paths.get(uploadDir).resolve(watermarkedFilename);

            // 确保上传目录存在
            File directory = new File(uploadDir);
            if (!directory.exists()) {
                directory.mkdirs();
            }

            // 3. 添加水印并保存
            try (InputStream inputStream = file.getInputStream()) {
                if ("text".equalsIgnoreCase(watermarkType)) {
                    watermarkService.addTextWatermark(inputStream, watermarkText, watermarkedFilePath.toFile());
                } else if ("image".equalsIgnoreCase(watermarkType)) {
                    // 假设水印图片路径在 application.properties 中配置 watermark.image.path
                    watermarkService.addImageWatermark(inputStream, watermarkedFilePath.toFile());
                } else {
                    redirectAttributes.addFlashAttribute("message", "无效的水印类型!");
                    return "redirect:/files/upload";
                }
            }

            redirectAttributes.addFlashAttribute("message", "文件 '" + originalFilename + "' 上传并添加水印成功,保存为: '" + watermarkedFilename + "'");
            redirectAttributes.addFlashAttribute("watermarkedFileUrl", "/files/download/" + watermarkedFilename);

        } catch (IOException e) {
            e.printStackTrace();
            redirectAttributes.addFlashAttribute("message", "文件上传失败: " + e.getMessage());
        } catch (IllegalArgumentException e) {
             e.printStackTrace();
            redirectAttributes.addFlashAttribute("message", "处理失败: " + e.getMessage());
        }

        return "redirect:/files/upload";
    }

    // 文件下载接口,用于查看带水印的图片
    @GetMapping("/download/{filename:.+}")
    @ResponseBody
    public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
        try {
            Path file = Paths.get(uploadDir).resolve(filename);
            Resource resource = new UrlResource(file.toUri());
            if (resource.exists() || resource.isReadable()) {
                String contentType = null;
                try {
                    // 尝试获取文件类型,对于图片尤其重要
                    contentType = java.nio.file.Files.probeContentType(file);
                } catch (IOException e) {
                    // log.info("Could not determine file type for: " + filename);
                }
                // 如果无法确定,则使用通用类型
                if (contentType == null) {
                    contentType = "application/octet-stream";
                }

                return ResponseEntity.ok()
                        .contentType(MediaType.parseMediaType(contentType))
                        .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"")
                        // inline 会尝试在浏览器中直接显示, attachment 会提示下载
                        .body(resource);
            } else {
                // throw new RuntimeException("Could not read the file!");
                return ResponseEntity.notFound().build();
            }
        } catch (MalformedURLException e) {
            // throw new RuntimeException("Error: " + e.getMessage());
            return ResponseEntity.badRequest().build();
        }
    }
}

默语解说:

代码语言:javascript
复制
@Controller:标记这是一个Spring MVC控制器。
代码语言:javascript
复制
@RequestMapping("/files"):此类下的所有请求路径都会以/files开头。
代码语言:javascript
复制
@Value("${file.upload-dir}"):从application.properties中注入我们自定义的文件存储路径。
代码语言:javascript
复制
showUploadForm():GET请求,返回一个名为upload-form.html的视图,我们稍后会创建它。
代码语言:javascript
复制
handleFileUploadWithWatermark():POST请求,处理实际的文件上传。
代码语言:javascript
复制
@RequestParam("file") MultipartFile file:Spring MVC会自动将名为file的上传部件绑定到MultipartFile对象。
代码语言:javascript
复制
MultipartFile是Spring对上传文件的封装,非常方便使用。
代码语言:javascript
复制
@RequestParam("watermarkText") String watermarkText:接收前端传来的水印文字。
代码语言:javascript
复制
redirectAttributes.addFlashAttribute():用于在重定向后依然能向页面传递消息(比如成功或失败的提示)。
代码语言:javascript
复制
serveFile():文件下载接口,用于在浏览器中查看或下载处理后的文件。
2.2 创建文件存储服务 (FileStorageService - 可选但推荐)

为了让Controller更专注于请求处理,我们可以把文件存储的逻辑抽取到一个Service中。

Java

代码语言:javascript
复制
package com.moyu.blog.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import jakarta.annotation.PostConstruct; // 注意: Spring Boot 3.x 使用 jakarta
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

@Service
public class FileStorageService {

    private final Path rootLocation;

    public FileStorageService(@Value("${file.upload-dir}") String uploadDir) {
        this.rootLocation = Paths.get(uploadDir);
    }

    @PostConstruct
    public void init() {
        try {
            Files.createDirectories(rootLocation);
            System.out.println("Upload directory created/initialized: " + rootLocation.toString());
        } catch (IOException e) {
            throw new RuntimeException("Could not initialize storage location", e);
        }
    }

    public String store(MultipartFile file) {
        try {
            if (file.isEmpty()) {
                throw new IllegalArgumentException("Failed to store empty file.");
            }
            String filename = System.currentTimeMillis() + "_" + file.getOriginalFilename();
            Path destinationFile = this.rootLocation.resolve(Paths.get(filename))
                    .normalize().toAbsolutePath();
            if (!destinationFile.getParent().equals(this.rootLocation.toAbsolutePath())) {
                // This is a security check
                throw new IllegalArgumentException("Cannot store file outside current directory.");
            }
            try (InputStream inputStream = file.getInputStream()) {
                Files.copy(inputStream, destinationFile, StandardCopyOption.REPLACE_EXISTING);
            }
            return filename;
        } catch (IOException e) {
            throw new RuntimeException("Failed to store file.", e);
        }
    }
}

*默语解说:*

代码语言:javascript
复制
@Service:标记这是一个1服务类。
代码语言:javascript
复制
@PostConstruct init():这个方法会在服务实例化后自动执行,我们用它来创建上传目录(如果不存在的话)。
代码语言:javascript
复制
store():核心的文件保存方法。它会生成一个唯一的文件名(避免重名覆盖),然后将上传文件的内容复制到目标路径。
2.3 创建一个简单的HTML上传表单 (src/main/resources/templates/upload-form.html)

HTML

代码语言:javascript
复制
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>默语博客 - 文件上传与水印</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
        .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
        h1 { color: #007bff; }
        .message { padding: 10px; margin-bottom: 15px; border-radius: 4px; }
        .success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        label { display: block; margin-bottom: 8px; font-weight: bold; }
        input[type="file"], input[type="text"], select {
            width: calc(100% - 22px); padding: 10px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px;
        }
        button {
            background-color: #007bff; color: white; padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px;
        }
        button:hover { background-color: #0056b3; }
        .preview img { max-width: 300px; max-height: 300px; margin-top: 15px; border: 1px solid #ddd; }
    </style>
</head>
<body>
<div class="container">
    <h1>上传图片并添加水印</h1>

    <div th:if="${message}" th:text="${message}"
         th:class="${(message != null and (message.contains('失败') or message.contains('请选择'))) ? 'message error' : 'message success'}">
    </div>

    <form method="POST" th:action="@{/files/upload-with-watermark}" enctype="multipart/form-data">
        <div>
            <label for="file">选择图片文件:</label>
            <input type="file" name="file" id="file" accept="image/*" required />
        </div>
        <div>
            <label for="watermarkType">选择水印类型:</label>
            <select name="watermarkType" id="watermarkType">
                <option value="text" selected>文字水印</option>
                <option value="image">图片LOGO水印</option>
            </select>
        </div>
        <div id="textWatermarkInput">
            <label for="watermarkText">输入水印文字 (可选):</label>
            <input type="text" name="watermarkText" id="watermarkText" placeholder="默语博客" />
        </div>
        <div>
            <button type="submit">上传并添加水印</button>
        </div>
    </form>

    <div th:if="${watermarkedFileUrl}" class="preview">
        <h3>带水印图片预览:</h3>
        <img th:src="${watermarkedFileUrl}" alt="Watermarked Image" />
        <p><a th:href="${watermarkedFileUrl}" target="_blank">查看或下载带水印的图片</a></p>
    </div>
</div>

<script>
    // 简单JS,根据选择显示/隐藏文字水印输入框
    const watermarkTypeSelect = document.getElementById('watermarkType');
    const textWatermarkDiv = document.getElementById('textWatermarkInput');
    watermarkTypeSelect.addEventListener('change', function() {
        if (this.value === 'text') {
            textWatermarkDiv.style.display = 'block';
        } else {
            textWatermarkDiv.style.display = 'none';
        }
    });
</script>

</body>
</html>

默语解说:

  • xmlns:th=“http://www.thymeleaf.org”:Thymeleaf的命名空间。
  • th:action=“@{/files/upload-with-watermark}”:表单提交的目标URL,@{…} 是Thymeleaf的URL表达式。

-enctype=“multipart/form-data” :文件上传表单必须设置这个属性。

代码语言:javascript
复制
input type="file" name="file":文件选择框,
代码语言:javascript
复制
name="file"要和Controller中@RequestParam("file")对应。
代码语言:javascript
复制
accept="image/*":限制用户只能选择图片文件。

th:if=“${message}”:用于显示Controller通过 RedirectAttributes传递过来的提示信息。

到这里,基本的文件上传框架就搭好了。你可以运行Spring Boot应用,访问 http://localhost:8080/files/upload 看看效果(此时还不能加水印)。

三、核心:实现水印添加服务 (WatermarkService)

这部分是我们的重头戏!我们将使用Java的java.awt包和java.awt.image包中的类来操作图片。

Java

代码语言:javascript
复制
package com.moyu.blog.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.imageio.ImageIO; // 注意:如果JDK 9+ 模块化,可能需要 --add-modules java.desktop
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;

@Service
public class WatermarkService {

    @Value("${watermark.image.path:classpath:watermark/default-logo.png}") // Provide a default if not set
    private String watermarkImagePath; // 从配置文件读取水印图片路径

    private static final int DEFAULT_WATERMARK_WIDTH = 120; // 默认图片水印宽度
    private static final float DEFAULT_ALPHA = 0.4f; // 默认透明度
    private static final int DEFAULT_TEXT_SIZE = 20; // 默认文字大小
    private static final Color DEFAULT_TEXT_COLOR = Color.LIGHT_GRAY; // 默认文字颜色

    /**
     * 给图片添加文字水印
     *
     * @param sourceImageStream 源图片输入流
     * @param text              水印文字
     * @param targetImageFile   添加水印后的图片输出文件
     * @throws IOException              IO异常
     * @throws IllegalArgumentException 参数异常
     */
    public void addTextWatermark(InputStream sourceImageStream, String text, File targetImageFile) throws IOException, IllegalArgumentException {
        if (sourceImageStream == null) {
            throw new IllegalArgumentException("源图片流不能为空");
        }
        if (!StringUtils.hasText(text)) {
            text = "Moyu Blog"; // 默认水印文字
        }
        if (targetImageFile == null) {
            throw new IllegalArgumentException("目标文件不能为空");
        }

        Image srcImg = ImageIO.read(sourceImageStream);
        if (srcImg == null) {
            throw new IllegalArgumentException("无法读取源图片,请确保是有效的图片格式!");
        }

        int srcImgWidth = srcImg.getWidth(null);
        int srcImgHeight = srcImg.getHeight(null);

        BufferedImage bufferedImage = new BufferedImage(srcImgWidth, srcImgHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = bufferedImage.createGraphics();

        // 1. 绘制原始图片
        g2d.drawImage(srcImg, 0, 0, srcImgWidth, srcImgHeight, null);

        // 2. 设置水印文字的属性
        g2d.setColor(DEFAULT_TEXT_COLOR);
        g2d.setFont(new Font("Arial", Font.BOLD, DEFAULT_TEXT_SIZE));
        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, DEFAULT_ALPHA)); // 设置透明度

        // 3. 计算文字位置(右下角)
        FontMetrics fm = g2d.getFontMetrics();
        int textWidth = fm.stringWidth(text);
        int textHeight = fm.getHeight(); // 获取字体高度,实际绘制时 y 是基线
        int x = srcImgWidth - textWidth - 10; // 10px padding from right
        int y = srcImgHeight - textHeight / 2 + 5; // 调整Y使文字更靠下边缘,10px padding from bottom

        // 4. 绘制水印文字 (可以绘制多次,形成平铺效果)
        g2d.drawString(text, x, y); // 在右下角绘制一个

        // 如果需要平铺效果(可选)
        // g2d.rotate(Math.toRadians(-30), bufferedImage.getWidth()/2.0, bufferedImage.getHeight()/2.0); //旋转
        // for (int i = -srcImgHeight / 2; i < srcImgHeight * 1.5; i += textHeight * 3) {
        //     for (int j = -srcImgWidth / 2; j < srcImgWidth * 1.5; j += textWidth * 2) {
        //         g2d.drawString(text, j, i);
        //     }
        // }


        g2d.dispose(); // 释放图形上下文使用的系统资源

        // 5. 输出图片
        String formatName = getFormatName(targetImageFile.getName());
        try (OutputStream out = Files.newOutputStream(targetImageFile.toPath())) {
            ImageIO.write(bufferedImage, formatName, out);
        }
        System.out.println("文字水印添加成功: " + targetImageFile.getAbsolutePath());
    }


    /**
     * 给图片添加图片水印
     *
     * @param sourceImageStream 源图片输入流
     * @param targetImageFile   添加水印后的图片输出文件
     * @throws IOException              IO异常
     * @throws IllegalArgumentException 参数异常
     */
    public void addImageWatermark(InputStream sourceImageStream, File targetImageFile) throws IOException, IllegalArgumentException {
        if (sourceImageStream == null) {
            throw new IllegalArgumentException("源图片流不能为空");
        }
        if (targetImageFile == null) {
            throw new IllegalArgumentException("目标文件不能为空");
        }

        Image srcImg = ImageIO.read(sourceImageStream);
        if (srcImg == null) {
            throw new IllegalArgumentException("无法读取源图片,请确保是有效的图片格式!");
        }

        int srcImgWidth = srcImg.getWidth(null);
        int srcImgHeight = srcImg.getHeight(null);

        BufferedImage bufferedImage = new BufferedImage(srcImgWidth, srcImgHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = bufferedImage.createGraphics();
        g2d.drawImage(srcImg, 0, 0, srcImgWidth, srcImgHeight, null);

        // 加载水印图片
        Image watermarkImg;
        try {
            ClassPathResource cpr = new ClassPathResource(watermarkImagePath.replace("classpath:", ""));
            if (!cpr.exists()) {
                 throw new IllegalArgumentException("水印图片未找到: " + watermarkImagePath);
            }
            watermarkImg = ImageIO.read(cpr.getInputStream());
        } catch (IOException e) {
            throw new IOException("无法加载水印图片: " + watermarkImagePath, e);
        }

        if (watermarkImg == null) {
            throw new IllegalArgumentException("无法读取水印图片,请确保是有效的图片格式!");
        }

        int watermarkWidth = watermarkImg.getWidth(null);
        int watermarkHeight = watermarkImg.getHeight(null);

        // 动态调整水印图片大小,使其宽度为原图的1/5,或不超过预设的最大/最小宽度
        // 这里简单处理,使用默认宽度,或者按比例缩放
        double scale = (double) DEFAULT_WATERMARK_WIDTH / watermarkWidth;
        int newWatermarkWidth = DEFAULT_WATERMARK_WIDTH;
        int newWatermarkHeight = (int) (watermarkHeight * scale);

        // 如果希望水印大小根据原图动态变化,可以这样:
        // int dynamicWatermarkWidth = srcImgWidth / 5; // 水印宽度为原图的1/5
        // double scale = (double) dynamicWatermarkWidth / watermarkWidth;
        // int newWatermarkWidth = dynamicWatermarkWidth;
        // int newWatermarkHeight = (int) (watermarkHeight * scale);


        // 设置透明度
        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, DEFAULT_ALPHA));

        // 计算水印位置(右下角)
        int x = srcImgWidth - newWatermarkWidth - 10; // 10px padding
        int y = srcImgHeight - newWatermarkHeight - 10; // 10px padding

        g2d.drawImage(watermarkImg, x, y, newWatermarkWidth, newWatermarkHeight, null);
        g2d.dispose();

        String formatName = getFormatName(targetImageFile.getName());
        try (OutputStream out = Files.newOutputStream(targetImageFile.toPath())) {
            ImageIO.write(bufferedImage, formatName, out);
        }
        System.out.println("图片水印添加成功: " + targetImageFile.getAbsolutePath());
    }

    /**
     * 根据文件名获取图片格式 (jpg, png等)
     */
    private String getFormatName(String fileName) {
        String extension = "";
        int i = fileName.lastIndexOf('.');
        if (i > 0) {
            extension = fileName.substring(i + 1);
        }
        if (StringUtils.hasText(extension) && (extension.equalsIgnoreCase("jpeg") || extension.equalsIgnoreCase("jpg"))) {
            return "JPEG"; // ImageIO.write 对于jpeg/jpg需要大写,或者直接用"jpg"
        }
        if (StringUtils.hasText(extension) && extension.equalsIgnoreCase("png")) {
            return "PNG";
        }
        // 默认或不支持的,尝试jpg
        return "JPEG";
    }
}

默语解说:

核心类:

代码语言:javascript
复制
java.awt.Image: 表示图像的抽象超类。
代码语言:javascript
复制
java.awt.image.BufferedImage: Image 的一个实现,它管理内存中的图像数据,我们可以直接操作其像素。
代码语言:javascript
复制
java.awt.Graphics2D: 提供了更复杂的图形操作API,如设置字体、颜色、透明度、绘制形状和图像等。它是从
代码语言:javascript
复制
BufferedImage对象获取的。
代码语言:javascript
复制
javax.imageio.ImageIO: 用于读取和写入图像的实用工具类。
代码语言:javascript
复制
java.awt.AlphaComposite: 用于控制绘制操作的透明度。SRC_ATOP

模式意味着将源(水印)绘制在目标(原图)之上,并根据源的alpha值混合。

addTextWatermark (添加文字水印) 方法:

读取源图片流到

代码语言:javascript
复制
Image对象。

创建一个与源图大小相同、支持ARGB(即带Alpha透明通道)的

代码语言:javascript
复制
BufferedImage。

获取

Graphics2D对象。

先绘制原始图片到bufferedImage上。这是基础!

设置水印文字的颜色、字体、透明度 (g2d.setComposite())。

使用FontMetrics计算文字的宽度和高度,以便精确定位(比如放在右下角)。

使用g2d.drawString()在指定位置绘制文字。

释放

代码语言:javascript
复制
Graphics2D资源 (g2d.dispose())。

使用

代码语言:javascript
复制
ImageIO.write()将处理后的bufferedImage

写入到目标文件。getFormatName是辅助方法,根据文件扩展名判断图片格式。

addImageWatermark (添加图片水印) 方法:

与文字水印类似,先读取源图、创建BufferedImage、获取Graphics2D、绘制原图。

使用

代码语言:javascript
复制
ClassPathResource从classpath加载水印图片 (logo.png)。

请确保你在src/main/resources下创建一个watermark文件夹,并放入一个名为logo.png的水印图片文件。

可以根据需要调整水印图片的大小(这里示例是固定宽度缩放)。

设置透明度。

计算图片水印的位置(比如右下角)。

使用g2d.drawImage()

将水印图片绘制到bufferedImage上。

释放资源,写入文件。

JDK 9+ 模块化注意:

如果你的项目使用JDK 9或更高版本,并且遇到了 java.awt 或 javax.imageio 包找不到的问题,这可能是因为Java平台的模块化系统。java.desktop模块(包含了AWT和Swing)默认可能不被所有类型的应用(特别是服务端应用)所包含。你可能需要在编译或运行时添加 --add-modules java.desktop 参数。不过,对于标准的Spring Boot Web应用,通常IDEA或Maven/Gradle配置会自动处理好。如果遇到问题,这是一个排查方向。

四、整合与测试

现在,我们的FileUploadControllerWatermarkService都已经准备好了。Controller会接收文件,然后调用WatermarkService中的方法来添加水印,并将结果保存。

运行和测试:

确保你的

代码语言:javascript
复制
application.properties中的file.upload-dir指向一个你机器上实际存在的、可写的目录。

如果你要测试图片水印,请在

代码语言:javascript
复制
src/main/resources/watermark/

目录下放置一个名为

代码语言:javascript
复制
logo.png的图片文件(或者修改application.properties中的
代码语言:javascript
复制
watermark.image.path为你自己的水印图片路径)。

运行你的Spring Boot主应用类 (通常是带有

@SpringBootApplication

注解的类)。

打开浏览器,访问

http://localhost:8080/files/upload

选择一个图片文件,选择水印类型(文字或图片),如果选择文字,可以输入自定义文字。

点击“上传并添加水印”。

如果一切顺利,页面会提示成功,并显示带水印图片的预览和下载链接。点击链接可以在浏览器中看到效果。

去你配置的

代码语言:javascript
复制
file.upload-dir目录下查看,应该能找到原始文件(如果你在controller中保存了的话)和带水印的文件(文件名通常以watermarked_开头)。

常见问题与调试:

文件路径问题:

确保

file.upload-dir和水印图片路径正确无误,且应用有读写权限。

图片格式:

代码语言:javascript
复制
ImageIO

支持常见的图片格式如JPEG, PNG, GIF, BMP。确保你上传的是这些格式。PNG格式更适合做水印,因为它支持透明通道。

依赖问题:

检查

代码语言:javascript
复制
pom.xml

中的依赖是否都已正确加载。

JDK版本: 如前所述,高版本JDK的模块化问题。

异常信息: 仔细阅读控制台输出的异常信息,它们通常会给出问题所在。

五、进阶思考与优化 (小白选读)

对于初学者,以上功能已经够用。但如果你想让它更强大,可以考虑:

水印位置可配置: 左上、右上、居中、平铺等。

水印样式更丰富: 旋转角度、字体大小颜色动态调整、图片水印大小动态调整。

异步处理:

对于大图片或大量上传,水印处理可能耗时。可以考虑使用异步任务(

代码语言:javascript
复制
@Async

)或消息队列来处理,避免阻塞HTTP请求线程。

使用专业图像处理库:

如 Thumbnailator或 Imgscalr,它们提供了更简洁的API和更优的性能来进行图片缩放、裁剪和水印等操作。

错误处理更完善: 对各种可能的异常(如文件不是图片、水印图片加载失败等)进行更细致的捕获和用户提示。

安全性:

防止路径遍历攻击(FileStorageService中的normalize().toAbsolutePath()和父目录检查有这个作用)。

总结

恭喜你,坚持看到了这里!通过今天的学习,我们一起用Spring Boot和Java AWT实现了图片上传和自动添加文字/图片水印的功能。我们从项目搭建、文件上传接口编写,到核心的水印服务实现,一步步剖析了整个过程。

回顾一下关键点:

  1. Spring Boot处理文件上传: 使用MultipartFile接口非常方便。
  2. Java AWT/Java2D图像处理: ImageIO, BufferedImage, Graphics2D 是我们的核心工具。
  3. 分离关注点: Controller负责请求,Service负责业务逻辑(文件存储、水印添加)。
  4. 配置化: 将文件路径、水印图片等信息放在配置文件中,便于修改。

希望这篇“默语牌”教程能让你对文件处理和图像操作更有信心。虽然我们只用了Java内置的库,但已经能实现很实用的功能了。记住,技术学习就是这样,从一个个小功能开始,不断积累,最终就能构建出复杂强大的系统。

参考资料:

Spring Boot官方文档 - 文件上传: https://spring.io/guides/gs/uploading-files/

Java™ Platform, Standard Edition 8 A

PI Specification -

代码语言:javascript
复制
java.awt.Graphics2D

:

https://docs.oracle.com/javase/8/docs/api/java/awt/Graphics2D.html

Java™ Platform, Standard Edition 8 API Specification -

代码语言:javascript
复制
javax.imageio.ImageIO

:

https://docs.oracle.com/javase/8/docs/api/javax/imageio/ImageIO.html

Baeldung - Adding Watermark to an Image in Java: (搜索类似关键词可以找到很多优秀教程)

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 默语带你玩转Spring Boot:图片上传后自动添加酷炫水印(超详细小白教程)
    • 正文
      • 一、项目环境准备与基本认知
      • 二、实现文件上传功能
      • 三、核心:实现水印添加服务 (WatermarkService)
      • 四、整合与测试
      • 五、进阶思考与优化 (小白选读)
    • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档