1、实现 MultipartFile
package com.pojo.common.core.domain;import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;public class InMultipartFile implements MultipartFile {private final String name;private String originalFilename;@Nullableprivate String contentType;private final byte[] content;/*** Create a new MockMultipartFile with the given content.* @param name the name of the file* @param content the content of the file*/public InMultipartFile(String name, @Nullable byte[] content) {this(name, "", null, content);}/*** Create a new MockMultipartFile with the given content.* @param name the name of the file* @param contentStream the content of the file as stream* @throws IOException if reading from the stream failed*/public InMultipartFile(String name, InputStream contentStream) throws IOException {this(name, "", null, FileCopyUtils.copyToByteArray(contentStream));}/*** Create a new MockMultipartFile with the given content.* @param name the name of the file* @param originalFilename the original filename (as on the client's machine)* @param contentType the content type (if known)* @param content the content of the file*/public InMultipartFile(String name, @Nullable String originalFilename, @Nullable String contentType, @Nullable byte[] content) {Assert.hasLength(name, "Name must not be null");this.name = name;this.originalFilename = (originalFilename != null ? originalFilename : "");this.contentType = contentType;this.content = (content != null ? content : new byte[0]);}/*** Create a new MockMultipartFile with the given content.* @param name the name of the file* @param originalFilename the original filename (as on the client's machine)* @param contentType the content type (if known)* @param contentStream the content of the file as stream* @throws IOException if reading from the stream failed*/public InMultipartFile(String name, @Nullable String originalFilename, @Nullable String contentType, InputStream contentStream)throws IOException {this(name, originalFilename, contentType, FileCopyUtils.copyToByteArray(contentStream));}@Overridepublic String getName() {return this.name;}@Overridepublic String getOriginalFilename() {return this.originalFilename;}@Override@Nullablepublic String getContentType() {return this.contentType;}@Overridepublic boolean isEmpty() {return (this.content.length == 0);}@Overridepublic long getSize() {return this.content.length;}@Overridepublic byte[] getBytes() throws IOException {return this.content;}@Overridepublic InputStream getInputStream() throws IOException {return new ByteArrayInputStream(this.content);}@Overridepublic void transferTo(File dest) throws IOException, IllegalStateException {FileCopyUtils.copy(this.content, dest);}}
2、添加水印工具类
package com.pojo.common.core.utils;import com.pojo.common.core.domain.InMultipartFile;
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.util.ArrayList;
import java.util.List;public class WatermarkUtil {/*** 添加多行文字水印** @param file 原始文件* @param lines 水印文本行列表* @param font 字体对象* @param color 颜色(支持透明度)* @param startXRatio 起始X坐标比例(0.0~1.0)* @param startYRatio 起始Y坐标比例(0.0~1.0)* @param lineSpacing 行间距倍数* @return 带水印的MultipartFile*/public static MultipartFile addTextWatermark(MultipartFile file,List<String> lines,Font font,Color color,float startXRatio,float startYRatio,float lineSpacing) throws IOException {// 读取原始图片(保留透明度通道)BufferedImage sourceImage = ImageIO.read(file.getInputStream());BufferedImage watermarkedImage = new BufferedImage(sourceImage.getWidth(),sourceImage.getHeight(),BufferedImage.TYPE_INT_ARGB);// 创建图形上下文Graphics2D g2d = watermarkedImage.createGraphics();configureGraphicsQuality(g2d);g2d.drawImage(sourceImage, 0, 0, null);// 设置水印样式g2d.setFont(font);g2d.setColor(color);g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, color.getAlpha() / 255f));// 计算实际绘制位置int baseX = (int) (sourceImage.getWidth() * startXRatio);int baseY = (int) (sourceImage.getHeight() * startYRatio);// 绘制多行文本drawWrappedText(g2d, lines, baseX, baseY, lineSpacing, sourceImage.getWidth());g2d.dispose();// 转换回MultipartFilereturn createOutputFile(watermarkedImage, file);}/*** 配置图形渲染质量*/private static void configureGraphicsQuality(Graphics2D g2d) {g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);}/*** 智能换行绘制*/private static void drawWrappedText(Graphics2D g2d, List<String> lines,int startX, int startY, float lineSpacing,int imageWidth) {FontMetrics metrics = g2d.getFontMetrics();int lineHeight = metrics.getHeight();int currentY = startY + metrics.getAscent();for (String line : lines) {List<String> wrappedLines = wrapChineseText(line, metrics, imageWidth - startX);for (String wrappedLine : wrappedLines) {int textWidth = metrics.stringWidth(wrappedLine);int x = calculateHorizontalPosition(startX, textWidth, imageWidth);g2d.drawString(wrappedLine, x, currentY);currentY += lineHeight * lineSpacing;}}}/*** 中文自动换行算法*/private static List<String> wrapChineseText(String text, FontMetrics metrics, int maxWidth) {List<String> result = new ArrayList<>();StringBuilder currentLine = new StringBuilder();int currentWidth = 0;for (int i = 0; i < text.length(); i++) {char c = text.charAt(i);int charWidth = metrics.charWidth(c);if (currentWidth + charWidth > maxWidth) {result.add(currentLine.toString());currentLine = new StringBuilder();currentWidth = 0;}currentLine.append(c);currentWidth += charWidth;}if (currentLine.length() > 0) {result.add(currentLine.toString());}return result;}/*** 计算水平位置(支持左对齐/居中/右对齐)*/private static int calculateHorizontalPosition(int startX, int textWidth, int imageWidth) {// 此处实现居中逻辑,可根据需要扩展return startX;}/*** 创建输出文件*/private static MultipartFile createOutputFile(BufferedImage image, MultipartFile originalFile)throws IOException {String formatName = getImageFormat(originalFile.getContentType());ByteArrayOutputStream baos = new ByteArrayOutputStream();if (!ImageIO.write(image, "png", baos)) {throw new IOException("不支持的图片格式: " + formatName);}return new InMultipartFile("watermarked." + formatName,originalFile.getOriginalFilename(),originalFile.getContentType(),baos.toByteArray());}/*** 从ContentType提取图片格式*/private static String getImageFormat(String contentType) {return contentType.substring("image/".length()).split(";")[0];}}
3、使用
// 准备水印参数List<String> watermarkLines = new ArrayList<>();watermarkLines.add("机密文件 严禁外传");watermarkLines.add("编号:2023-0012");watermarkLines.add("有效期至:2025-12-31");// 创建字体(建议使用物理字体文件更可靠)Font font = new Font("微软雅黑", Font.BOLD, 16);Color color = new Color(255, 0, 0, 180); // 半透明白色MultipartFile result = null;// 添加水印try {result = WatermarkUtil.addTextWatermark(file,watermarkLines,font,color,0.05f, // 左侧5%位置0.7f, // 顶部70%位置(靠近底部)1.0f // 1.0倍行间距);} catch (IOException e) {throw new RuntimeException(e);}
4、测试效果