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;
@Nullable
private 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));
}
@Override
public String getName() {
return this.name;
}
@Override
public String getOriginalFilename() {
return this.originalFilename;
}
@Override
@Nullable
public String getContentType() {
return this.contentType;
}
@Override
public boolean isEmpty() {
return (this.content.length == 0);
}
@Override
public long getSize() {
return this.content.length;
}
@Override
public byte[] getBytes() throws IOException {
return this.content;
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(this.content);
}
@Override
public 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();
// 转换回MultipartFile
return 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、测试效果