java项目文件水印功能实现

java项目文件水印功能实现

Posted by John Doe on 2024-10-09
Words 1.6k and Reading Time 7 Minutes
Viewed Times

背景

需求要求实现pdf,word,png,jpg等格式文件添加水印并下载的功能,采用了策略模式易于扩展,特此记录实现方式。

下载实现

常见的input、output流实现,这里以mongoDB获取输入流为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Autowired
private GridFSBucket gridFSBucket;

@Autowired
private GridFsTemplate gridFsTemplate;

@Autowired
private List<WatermarkService> watermarkServiceList;


public void fileDownload(String fileId, HttpServletResponse response, HttpServletRequest request, String watermark) {
try {
DocumentDesc documentDesc= DBDao.getFile(fileId);

// 从mongoDB获取文件流
Query query = Query.query(Criteria.where("_id").is(documentDesc.getDocStorePath()));
GridFSFile gridFSFile = gridFsTemplate.findOne(query);
GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId());
GridFsResource gridFsResource = new GridFsResource(gridFSFile, gridFSDownloadStream);
InputStream inputStream = gridFsResource.getInputStream();

// 获取文件类型
String[] split = documentDesc.getFileName().split("\\.");
String fileType = split[split.length - 1];

// 策略模式根据不同的文件类型下载文件
WatermarkService watermarkService = watermarkServiceList.stream().filter(o -> o.support(fileType)).findAny().orElse(null);

OutputStream sos = response.getOutputStream();
String name = documentDesc.getFileName();
response.setContentType("application/octet-stream;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
// 设置下载文件名
String fileName = URLEncoder.encode(name, "UTF-8").replaceAll("\\+", "%20");
// 设置下载文件名
response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
watermarkService.add(inputStream, sos, watermark);
// 向客户端输出文件
IOUtils.copy(inputStream, sos);
sos.flush();
sos.close();
logger.info("fileId {} success ", fileId);
} catch (Exception e) {
log.error("下载文件失败:", e);
throw new BizException("下载文件失败");
}
}

使用策略模式划分功能

使用策略模式可以更方便的扩展功能,增强代码可读性维护性,避免过多的if-else判断。

模板接口

后续的各种类型文件需实现该接口

1
2
3
4
5
6
7
public interface WatermarkService {

boolean support(String type);

OutputStream add(InputStream inputStream, OutputStream outputStream, String text) throws Exception;

}

使用stream流优雅分类

1
2
3
4
5
6
7
8
9
@Autowired
private List<WatermarkService> watermarkServiceList;

// 文件后缀名满足support方法的service会被过滤出来,后续调用add()方法即可添加对应文件类型的水印
WatermarkService watermarkService = watermarkServiceList.stream().filter(o -> o.support(fileType)).findAny().orElse(null);

...
// 调用add()方法添加水印
watermarkService.add(inputStream, sos, watermark);

各种文件类型的具体实现方式

多数水印功能的实现的通过创建一个图片文件,然后将该图片文件添加到指定的pdf、word、excel文件上,下面是自定义图片类的代码,包含图片的存储地址,长和宽。

1
2
3
4
5
6
7
@Getter
@AllArgsConstructor
public static class ImageMessage {
private int width;
private int height;
private String filePath;
}

1.pdf

这里使用的是apache.pdfbox.pdmodel.PDDocument包进行pdf操作, 具体实现步骤为:1.获取文件流。2.创建需要添加的水印图片样式。3.将创建好的水印图片添加到pdf上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import org.apache.pdfbox.pdmodel.PDDocument;

@Slf4j
@Service
public class PdfWatermarkServiceImpl implements WatermarkService {

@Override
public boolean support(String type) {
return StringUtils.equalsIgnoreCase("PDF", type);
}


@Override
public OutputStream add(InputStream inputStream, OutputStream outputStream, String text) throws IOException {
if (ignore(text)) return outputStream;
try {
PDDocument doc = PDDocument.load(inputStream);
// 移除pdf加密策略,否则可能获取pdf文件失败
doc.setAllSecurityToBeRemoved(true);

// 计算对角线长度,参考Math.sqrt(width * width + height * height)
Double diagonal = getDiagonal(doc);

// 1.1创建水印图片
ImageGeneratorUtil.ImageMessage image = getImage(text, diagonal);

// 1.2将水印图片添加到pdf文件上,具体实现见下文
PdfWatermarkUtil.addSingleDiagonalWaterMarkToPdf(doc, image, outputStream);

} catch (Exception e) {
log.error("exception message ", e);
}
return outputStream;
}

// 1.1 绘制水印图片,下面代码是示例代码,具体的水印文本重复次数、字体格式、偏转角度请自行计算
public static ImageMessage getImage(String text, Double maxWidth) throws IOException {
BufferedImage image = generateTransparentImage(text, maxWidth);
......
return new ImageMessage(image.getWidth(), image.getHeight(), pathname.toAbsolutePath().toString());
}


public static BufferedImage generateTransparentImage(String text, double diagonalLength) {
// 初始字体大小
float fontSize = 8.0f;
Font font;
// 预留一部分对角线长度
diagonalLength = 0.8f * diagonalLength;

BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

FontRenderContext frc = g2d.getFontRenderContext();
Rectangle2D bounds;
double currentDiagonal;
int width = 0, height = 0; // 在循环外部声明 width 和 height
font = FontLoaderUtil.getFont(fontSize);

//todo 使用对应的字体格式

g2d.dispose();

// 使用找到的字体大小和计算出的 width 和 height 重新创建图像
image = new BufferedImage(width + 20, height, BufferedImage.TYPE_INT_ARGB);
g2d = image.createGraphics();
setGraphicsHints(g2d);
g2d.setFont(font);
drawText(g2d, text, width, height, frc); // 确保 drawText 方法可以访问到正确的 width 和 height

g2d.dispose();
return image;
}



// 1.2 向PDF添加水印图片
public static void addSingleDiagonalWaterMarkToPdf(PDDocument doc, ImageGeneratorUtil.ImageMessage img, OutputStream outputStream) throws IOException {
try {
// 移除所有安全设置,包括加密
doc.setAllSecurityToBeRemoved(true);

// 获取水印图片
PDImageXObject pdImage = PDImageXObject.createFromFile(img.getFilePath(), doc);

//对水印图片进行翻转等操作,比如斜着的文字水印
WatermarkOptions options = new WatermarkOptions()
.size(img.getWidth(), img.getHeight())
.padding(10)
.layout(4, 3)
.rotate(30);

// for循环每一页添加水印图片
for (PDPage page : doc.getPages()) {
addSingleDiagonalWaterMarkToPdf(doc, page, pdImage, options);
}
doc.save(outputStream);
} finally {
// 删除水印图片
boolean delete = new File(img.getFilePath()).delete();
}

}

}

2.png,jpg,jpeg

接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Slf4j
@Service
public class ImageWatermarkServiceImpl implements WatermarkService {
@Override
public boolean support(String type) {
return StringUtils.equalsAnyIgnoreCase(type, "PNG", "JPG", "JPEG");
}

@Override
public OutputStream add(InputStream inputStream, OutputStream outputStream, String text) throws IOException {
if (ignore(text)) return outputStream;
try {
// 水印字体颜色
PictureWatermarkUtil waterMarker = new PictureWatermarkUtil(text);
ImageIO.write(makeBottomRightWaterMark(inputStream), "PNG", outputStream)
} catch (Exception e) {
log.error("图片添加水印并下载失败");
}
return outputStream;
}

/**
* 在指定图片右下角添加上文本水印
*
* @param inputStream 需要加水印图片的输入流(不会修改原图)
* @return 添加水印后的图片对象
*/
public BufferedImage makeBottomRightWaterMark(InputStream inputStream) throws IOException {
BufferedImage originalImage = ImageIO.read(inputStream);
BufferedImage bufferedImage = new BufferedImage(originalImage.getWidth(), originalImage.getHeight(), originalImage.getType());
Graphics2D g2 = bufferedImage.createGraphics(); // 获取图片画布
// 文本抗锯齿
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setFont(waterMarkFont); // 设置字体
g2.setPaint(waterMarkPaint); // 设置字体颜色
g2.drawImage(originalImage, 0, 0, null);
int hoffset = 20; // 水平方向与最右的偏移量
int voffset = 20; // 垂直方向与最低端的偏移量
g2.drawString(customText,
bufferedImage.getWidth() - getTextLength(g2, customText) - hoffset,
bufferedImage.getHeight() - voffset);
g2.dispose();
return bufferedImage;
}

}

3.word

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

@Slf4j
@Service
public class WordWatermarkServiceImpl implements WatermarkService {
@Override
public boolean support(String type) {
return StringUtils.equalsIgnoreCase("DOCX", type);
}

@Override
public OutputStream add(InputStream inputStream, OutputStream outputStream, String text) throws IOException {
if (ignore(text)) return outputStream;
try {
// todo业务自定义代码,如字体透明度要求等具体实现
// 添加水印
makeSlopeWaterMark(inputStream, outputStream);
IOUtils.copy(inputStream, outputStream);
} catch (Exception e) {
log.error("exception message ", e);
}
return outputStream;
}

/**
* 【核心方法】将输入流中的docx文档加载添加水印后输出到输出流中.
*
* @param inputStream docx文档输入流
* @param outputStream 添加水印后docx文档的输出流
*/
public void makeSlopeWaterMark(InputStream inputStream, OutputStream outputStream) {
Path tempFile = createTempFile(inputStream);
if (tempFile == null) {
return;
}
try (BufferedInputStream buffIn = new BufferedInputStream(Files.newInputStream(tempFile))) {
XWPFDocument doc = loadDocXDocument(buffIn, outputStream);
if (doc == null) {
return;
}
// 遍历文档,添加水印
for (int lineIndex = -10; lineIndex < 10; lineIndex++) {
//到顶部距离
styleTop = 200 * lineIndex + " ";
waterMarkDocXDocument(doc);
}
try {
doc.write(outputStream); // 写出添加水印后的文档
} finally {
IOUtils.closeQuietly(doc);
}
} catch (Exception exception) {
throw new RuntimeException("水印添加失败!");
} finally {
deleteFile(tempFile);
}
}
}

This is copyright.

...

...

00:00
00:00