From e45c112a1f024aed51a0380dcfcd35ee56e150e2 Mon Sep 17 00:00:00 2001 From: jack ning Date: Wed, 25 Jun 2025 17:31:55 +0800 Subject: [PATCH] update --- .../bytedesk/core/upload/UploadRequest.java | 9 +- .../upload/UploadRestControllerVisitor.java | 23 ++ .../core/upload/UploadRestService.java | 210 ++++++++++++- .../bytedesk/core/upload/watermark/README.md | 220 +++++++++++++ .../upload/watermark/WATERMARK_SUMMARY.md | 200 ++++++++++++ .../upload/watermark/WatermarkConfig.java | 83 +++++ .../upload/watermark/WatermarkService.java | 289 ++++++++++++++++++ .../application-watermark-example.yml | 35 +++ .../watermark/WatermarkServiceTest.java | 192 ++++++++++++ 9 files changed, 1251 insertions(+), 10 deletions(-) create mode 100644 modules/core/src/main/java/com/bytedesk/core/upload/watermark/README.md create mode 100644 modules/core/src/main/java/com/bytedesk/core/upload/watermark/WATERMARK_SUMMARY.md create mode 100644 modules/core/src/main/java/com/bytedesk/core/upload/watermark/WatermarkConfig.java create mode 100644 modules/core/src/main/java/com/bytedesk/core/upload/watermark/WatermarkService.java create mode 100644 modules/core/src/main/resources/application-watermark-example.yml create mode 100644 modules/core/src/test/java/com/bytedesk/core/upload/watermark/WatermarkServiceTest.java diff --git a/modules/core/src/main/java/com/bytedesk/core/upload/UploadRequest.java b/modules/core/src/main/java/com/bytedesk/core/upload/UploadRequest.java index e2cd95f482..cda8bb0222 100644 --- a/modules/core/src/main/java/com/bytedesk/core/upload/UploadRequest.java +++ b/modules/core/src/main/java/com/bytedesk/core/upload/UploadRequest.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-03-18 12:06:26 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2025-04-23 16:38:21 + * @LastEditTime: 2025-06-25 17:28:23 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -57,4 +57,11 @@ public class UploadRequest extends BaseRequest { private String visitorAvatar; // 游客头像 // private String extra; // 额外附加信息 + + // 水印相关字段 + private Boolean addWatermark; // 是否添加水印 + private String watermarkText; // 水印文字 + private String watermarkPosition; // 水印位置 + private Integer watermarkFontSize; // 水印字体大小 + private String watermarkColor; // 水印颜色 } diff --git a/modules/core/src/main/java/com/bytedesk/core/upload/UploadRestControllerVisitor.java b/modules/core/src/main/java/com/bytedesk/core/upload/UploadRestControllerVisitor.java index 85314d7146..673f55b2f6 100644 --- a/modules/core/src/main/java/com/bytedesk/core/upload/UploadRestControllerVisitor.java +++ b/modules/core/src/main/java/com/bytedesk/core/upload/UploadRestControllerVisitor.java @@ -44,5 +44,28 @@ public class UploadRestControllerVisitor { return ResponseEntity.ok(JsonResult.success("upload success", response)); } + + // 文件上传(支持水印控制) + @PostMapping("/file/watermark") + public ResponseEntity uploadWithWatermark( + @RequestParam("file") MultipartFile file, + UploadRequest request, + @RequestParam(value = "addWatermark", defaultValue = "true") boolean addWatermark, + @RequestParam(value = "watermarkText", required = false) String watermarkText, + @RequestParam(value = "watermarkPosition", required = false) String watermarkPosition) { + + // 设置水印相关参数 + if (watermarkText != null && !watermarkText.trim().isEmpty()) { + request.setWatermarkText(watermarkText); + } + if (watermarkPosition != null && !watermarkPosition.trim().isEmpty()) { + request.setWatermarkPosition(watermarkPosition); + } + request.setAddWatermark(addWatermark); + + UploadResponse response = uploadService.handleFileUpload(file, request); + + return ResponseEntity.ok(JsonResult.success("upload success", response)); + } } diff --git a/modules/core/src/main/java/com/bytedesk/core/upload/UploadRestService.java b/modules/core/src/main/java/com/bytedesk/core/upload/UploadRestService.java index 9ea95a5633..88fead55d3 100755 --- a/modules/core/src/main/java/com/bytedesk/core/upload/UploadRestService.java +++ b/modules/core/src/main/java/com/bytedesk/core/upload/UploadRestService.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-03-15 11:35:53 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2025-06-17 17:24:05 + * @LastEditTime: 2025-06-25 17:30:49 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -25,6 +25,10 @@ import java.util.Optional; import java.util.stream.Stream; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import javax.imageio.ImageIO; import org.modelmapper.ModelMapper; import org.springframework.core.io.Resource; @@ -47,6 +51,9 @@ import com.bytedesk.core.upload.storage.UploadStorageFileNotFoundException; import com.bytedesk.core.utils.BdDateUtils; import com.bytedesk.core.utils.ConvertUtils; +import com.bytedesk.core.upload.watermark.WatermarkConfig; +import com.bytedesk.core.upload.watermark.WatermarkService; + import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -68,6 +75,10 @@ public class UploadRestService extends BaseRestService queryByOrg(UploadRequest request) { Pageable pageable = request.getPageable(); @@ -78,14 +89,23 @@ public class UploadRestService extends BaseRestService queryByUser(UploadRequest request) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'queryByUser'"); + UserEntity user = authService.getUser(); + if (user == null) { + throw new RuntimeException("用户未登录"); + } + request.setUserUid(user.getUid()); + // request.setOrgUid(user.getOrgUid()); + return queryByOrg(request); } @Override public UploadResponse queryByUid(UploadRequest request) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'queryByUid'"); + Optional uploadOptional = findByUid(request.getUid()); + if (uploadOptional.isPresent()) { + return convertToResponse(uploadOptional.get()); + } else { + throw new RuntimeException("Upload with uid '" + request.getUid() + "' not found"); + } } @Override @@ -93,8 +113,8 @@ public class UploadRestService extends BaseRestService watermarkConfig.getMaxImageSize() || height > watermarkConfig.getMaxImageSize()) { + log.debug("图片尺寸太大,不添加水印: {}x{}", width, height); + return false; + } + } + } catch (IOException e) { + log.warn("无法读取图片尺寸,跳过水印检查: {}", file.getOriginalFilename(), e); + return false; + } + + return true; + } + + /** + * 为文件添加水印(支持自定义参数) + */ + private void addWatermarkToFile(MultipartFile file, Path destinationPath, UploadRequest request) throws IOException { + try { + // 读取原始图片 + BufferedImage originalImage = ImageIO.read(file.getInputStream()); + if (originalImage == null) { + log.error("无法读取图片文件: {}", file.getOriginalFilename()); + // 如果无法读取图片,直接保存原文件 + try (InputStream inputStream = file.getInputStream()) { + Files.copy(inputStream, destinationPath, StandardCopyOption.REPLACE_EXISTING); + } + return; + } + + // 获取水印参数 + String watermarkText = getWatermarkText(request); + WatermarkService.WatermarkPosition position = getWatermarkPosition(request); + int fontSize = getWatermarkFontSize(request); + Color watermarkColor = getWatermarkColor(request); + + // 添加水印 + byte[] watermarkedImageBytes = watermarkService.addTextWatermark( + originalImage, + watermarkText, + position, + fontSize, + watermarkColor + ); + + // 保存到文件 + try (InputStream inputStream = new ByteArrayInputStream(watermarkedImageBytes)) { + Files.copy(inputStream, destinationPath, StandardCopyOption.REPLACE_EXISTING); + } + + log.info("成功为图片添加水印: {}", file.getOriginalFilename()); + + } catch (Exception e) { + log.error("添加水印失败,保存原文件: {}", file.getOriginalFilename(), e); + // 如果添加水印失败,保存原文件 + try (InputStream inputStream = file.getInputStream()) { + Files.copy(inputStream, destinationPath, StandardCopyOption.REPLACE_EXISTING); + } + } + } + + /** + * 获取水印文字 + */ + private String getWatermarkText(UploadRequest request) { + if (request != null && request.getWatermarkText() != null && !request.getWatermarkText().trim().isEmpty()) { + return request.getWatermarkText(); + } + return watermarkConfig.getText(); + } + + /** + * 获取水印位置 + */ + private WatermarkService.WatermarkPosition getWatermarkPosition(UploadRequest request) { + if (request != null && request.getWatermarkPosition() != null && !request.getWatermarkPosition().trim().isEmpty()) { + try { + return WatermarkService.WatermarkPosition.valueOf(request.getWatermarkPosition().toUpperCase()); + } catch (IllegalArgumentException e) { + log.warn("无效的水印位置: {}, 使用默认位置", request.getWatermarkPosition()); + } + } + return watermarkConfig.getPosition(); + } + + /** + * 获取水印字体大小 + */ + private int getWatermarkFontSize(UploadRequest request) { + if (request != null && request.getWatermarkFontSize() != null && request.getWatermarkFontSize() > 0) { + return request.getWatermarkFontSize(); + } + return watermarkConfig.getFontSize(); + } + + /** + * 获取水印颜色 + */ + private Color getWatermarkColor(UploadRequest request) { + if (request != null && request.getWatermarkColor() != null && !request.getWatermarkColor().trim().isEmpty()) { + return parseColor(request.getWatermarkColor()); + } + return parseColor(watermarkConfig.getColor()); + } + + /** + * 解析颜色字符串 + */ + private Color parseColor(String colorStr) { + try { + String[] parts = colorStr.split(","); + if (parts.length == 4) { + int r = Integer.parseInt(parts[0].trim()); + int g = Integer.parseInt(parts[1].trim()); + int b = Integer.parseInt(parts[2].trim()); + int a = Integer.parseInt(parts[3].trim()); + return new Color(r, g, b, a); + } else if (parts.length == 3) { + int r = Integer.parseInt(parts[0].trim()); + int g = Integer.parseInt(parts[1].trim()); + int b = Integer.parseInt(parts[2].trim()); + return new Color(r, g, b, 128); // 默认透明度 + } + } catch (Exception e) { + log.warn("无法解析颜色配置: {}, 使用默认颜色", colorStr, e); + } + return new Color(255, 255, 255, 128); // 默认白色半透明 + } + public Stream loadAll() { try { return Files.walk(this.uploadDir, 1) @@ -329,7 +521,7 @@ public class UploadRestService extends BaseRestService 0); + + // 验证生成的图片可以正常读取 + try { + BufferedImage watermarkedImage = ImageIO.read(new ByteArrayInputStream(watermarkedImageBytes)); + assertNotNull(watermarkedImage); + assertEquals(400, watermarkedImage.getWidth()); + assertEquals(300, watermarkedImage.getHeight()); + } catch (IOException e) { + fail("无法读取水印图片: " + e.getMessage()); + } + } + + @Test + void testAddTextWatermarkWithCustomParameters() { + // 测试自定义参数的文字水印 + byte[] watermarkedImageBytes = watermarkService.addTextWatermark( + testImage, + "Custom Watermark", + WatermarkService.WatermarkPosition.CENTER, + 32, // 字体大小 + new Color(255, 0, 0, 128) // 红色半透明 + ); + + assertNotNull(watermarkedImageBytes); + assertTrue(watermarkedImageBytes.length > 0); + } + + @Test + void testAddImageWatermark() { + // 创建一个水印图片 + BufferedImage watermarkImage = new BufferedImage(50, 30, BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < 50; x++) { + for (int y = 0; y < 30; y++) { + watermarkImage.setRGB(x, y, new Color(0, 0, 255, 128).getRGB()); + } + } + + // 测试添加图片水印 + byte[] watermarkedImageBytes = watermarkService.addImageWatermark( + testImage, + watermarkImage, + WatermarkService.WatermarkPosition.TOP_LEFT, + 0.5f + ); + + assertNotNull(watermarkedImageBytes); + assertTrue(watermarkedImageBytes.length > 0); + } + + @Test + void testIsImageFile() { + // 测试图片文件检测 + MockMultipartFile imageFile = new MockMultipartFile( + "file", + "test.jpg", + "image/jpeg", + "fake image content".getBytes() + ); + + assertTrue(watermarkService.isImageFile(imageFile)); + + // 测试非图片文件 + MockMultipartFile textFile = new MockMultipartFile( + "file", + "test.txt", + "text/plain", + "text content".getBytes() + ); + + assertFalse(watermarkService.isImageFile(textFile)); + + // 测试空文件 + MockMultipartFile emptyFile = new MockMultipartFile( + "file", + "test.jpg", + "image/jpeg", + new byte[0] + ); + + assertFalse(watermarkService.isImageFile(emptyFile)); + } + + @Test + void testWatermarkPositions() { + // 测试所有水印位置 + WatermarkService.WatermarkPosition[] positions = { + WatermarkService.WatermarkPosition.TOP_LEFT, + WatermarkService.WatermarkPosition.TOP_RIGHT, + WatermarkService.WatermarkPosition.BOTTOM_LEFT, + WatermarkService.WatermarkPosition.BOTTOM_RIGHT, + WatermarkService.WatermarkPosition.CENTER + }; + + for (WatermarkService.WatermarkPosition position : positions) { + byte[] watermarkedImageBytes = watermarkService.addTextWatermark( + testImage, + "Position Test", + position + ); + + assertNotNull(watermarkedImageBytes); + assertTrue(watermarkedImageBytes.length > 0); + } + } + + @Test + void testNullImage() { + // 测试空图片处理 + assertThrows(RuntimeException.class, () -> { + watermarkService.addTextWatermark( + null, + "Test", + WatermarkService.WatermarkPosition.BOTTOM_RIGHT + ); + }); + } + + @Test + void testEmptyWatermarkText() { + // 测试空水印文字 + byte[] watermarkedImageBytes = watermarkService.addTextWatermark( + testImage, + "", + WatermarkService.WatermarkPosition.BOTTOM_RIGHT + ); + + assertNotNull(watermarkedImageBytes); + assertTrue(watermarkedImageBytes.length > 0); + } +} \ No newline at end of file