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 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 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 forPage(List watermarks, int pageNumber, int totalPages) { List 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; } }