刘光辉
4 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
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
107
108
109
110
111
package jnpf.fileStorage.dms;
 
import jnpf.dmsEntity.preview.model.DmsWatermarkSnapshot;
import jnpf.exception.DataException;
import lombok.RequiredArgsConstructor;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.util.Matrix;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
 
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
 
@Service
@RequiredArgsConstructor
public class DmsPdfWatermarkService {
    private static final double POINTS_PER_MM = 72D / 25.4D;
    private static final int MAX_PAGES = 1000;
    private static final int MAX_OUTPUT_BYTES = 200 * 1024 * 1024;
 
    private final DmsDocxWatermarkService rasterizer;
 
    public byte[] apply(byte[] source, List<DmsWatermarkSnapshot> watermarks) {
        if (watermarks == null || watermarks.isEmpty()) return source;
        if (source == null || source.length == 0) throw new DataException("PDF 内容为空,不能生成水印");
        try (PDDocument document = Loader.loadPDF(source);
             ByteArrayOutputStream output = new ByteArrayOutputStream()) {
            int totalPages = document.getNumberOfPages();
            if (totalPages < 1 || totalPages > MAX_PAGES) {
                throw new DataException("PDF 页数超出动态水印处理范围(1-1000 页)");
            }
            for (DmsWatermarkSnapshot watermark : watermarks) {
                if (watermark == null) throw new DataException("PDF 水印配置无效");
                if (watermark.getRenderPages() != null && watermark.getRenderPages().stream()
                        .anyMatch(page -> page == null || page < 1 || page > totalPages)) {
                    throw new DataException("PDF 水印指定页码超出文件页数");
                }
            }
            for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
                int pageNumber = pageIndex + 1;
                List<DmsWatermarkSnapshot> selected = forPage(watermarks, pageNumber, totalPages);
                if (selected.isEmpty()) continue;
                PDPage page = document.getPage(pageIndex);
                PDRectangle box = page.getCropBox();
                if (box == null || box.getWidth() <= 0 || box.getHeight() <= 0) {
                    throw new DataException("PDF 页面尺寸无效,不能生成水印");
                }
                int rotation = Math.floorMod(page.getRotation(), 360);
                boolean quarterTurn = rotation == 90 || rotation == 270;
                float visualWidth = quarterTurn ? box.getHeight() : box.getWidth();
                float visualHeight = quarterTurn ? box.getWidth() : box.getHeight();
                byte[] overlay = rasterizer.renderPdfOverlay(visualWidth / POINTS_PER_MM,
                        visualHeight / POINTS_PER_MM, selected);
                PDImageXObject image = PDImageXObject.createFromByteArray(document, overlay,
                        "dms-watermark-page-" + pageNumber);
                try (PDPageContentStream stream = new PDPageContentStream(document, page,
                        PDPageContentStream.AppendMode.APPEND, true, true)) {
                    stream.transform(pageTransform(box, rotation));
                    stream.drawImage(image, 0, 0, visualWidth, visualHeight);
                }
            }
            document.save(output);
            if (output.size() > MAX_OUTPUT_BYTES) {
                throw new DataException("PDF 水印输出超过处理上限(200 MB)");
            }
            return output.toByteArray();
        } catch (DataException exception) {
            throw exception;
        } catch (Exception exception) {
            throw new DataException("生成 PDF 水印失败:" + exception.getMessage());
        }
    }
 
    private Matrix pageTransform(PDRectangle box, int rotation) {
        // PDF /Rotate changes the displayed page, while CropBox keeps its unrotated dimensions.
        float x = box.getLowerLeftX();
        float y = box.getLowerLeftY();
        float width = box.getWidth();
        float height = box.getHeight();
        switch (rotation) {
            case 0: return new Matrix(1, 0, 0, 1, x, y);
            case 90: return new Matrix(0, 1, -1, 0, x + width, y);
            case 180: return new Matrix(-1, 0, 0, -1, x + width, y + height);
            case 270: return new Matrix(0, -1, 1, 0, x, y + height);
            default: throw new DataException("PDF 页面旋转角度无效");
        }
    }
 
    private List<DmsWatermarkSnapshot> forPage(List<DmsWatermarkSnapshot> watermarks,
                                                int pageNumber, int totalPages) {
        List<DmsWatermarkSnapshot> selected = new ArrayList<>();
        for (DmsWatermarkSnapshot watermark : watermarks) {
            if (watermark.getRenderPages() != null
                    && !watermark.getRenderPages().contains(pageNumber)) continue;
            DmsWatermarkSnapshot copy = new DmsWatermarkSnapshot();
            BeanUtils.copyProperties(watermark, copy);
            if ("TEXT".equalsIgnoreCase(copy.getWatermarkType()) && copy.getContent() != null) {
                copy.setContent(copy.getContent().replace("{/CurrPage}", String.valueOf(pageNumber))
                        .replace("{/TotalPage}", String.valueOf(totalPages)));
            }
            selected.add(copy);
        }
        return selected;
    }
}