diff --git a/WATERMARK_API_DOC.md b/WATERMARK_API_DOC.md new file mode 100644 index 0000000..89c4fbe --- /dev/null +++ b/WATERMARK_API_DOC.md @@ -0,0 +1,128 @@ +# 水印功能模块 API 文档 + +## 概述 +水印功能模块提供了为图片添加自定义水印的能力,支持透明度、密度、字体样式、位置等多种配置选项。 + +## 核心类说明 + +### 1. WatermarkPosition.java (位置枚举) +定义水印显示位置: +- `TILED` - 平铺模式(默认) +- `TOP_LEFT` - 左上角 +- `TOP_RIGHT` - 右上角 +- `BOTTOM_LEFT` - 左下角 +- `BOTTOM_RIGHT` - 右下角 +- `CENTER` - 居中 + +### 2. WatermarkConfiguration.java (配置类) +水印配置类,支持链式调用: + +| 方法 | 参数 | 说明 | 默认值 | +|------|------|------|--------| +| `setText(String)` | 水印文本 | 设置水印内容 | "水印" | +| `setOpacity(float)` | 0-1之间的浮点数 | 设置透明度 | 0.3f | +| `setHorizontalSpacing(int)` | 像素值 | 设置水平间距 | 200 | +| `setVerticalSpacing(int)` | 像素值 | 设置垂直间距 | 150 | +| `setFontFamily(String)` | 字体名称 | 设置字体 | "宋体" | +| `setFontSize(int)` | 字号 | 设置字体大小 | 30 | +| `setColor(Color)` | Color对象 | 设置字体颜色 | Color.GRAY | +| `setBold(Boolean)` | true/false | 是否加粗 | false | +| `setRotation(Double)` | 角度值 | 设置旋转角度 | -30.0 | +| `setPosition(WatermarkPosition)` | 位置枚举 | 设置显示位置 | TILED | + +### 3. WatermarkUtil.java (工具类) +水印核心功能类,提供静态方法: + +| 方法 | 说明 | +|------|------| +| `createWatermarkCanvas(width, height, config)` | 创建水印画布 | +| `createWatermarkCanvasAndStore(width, height, config, path)` | 创建水印并保存到文件 | +| `addWatermarkToImage(targetImage, config)` | 为已有图片添加水印 | +| `addWatermarkToImageAndStore(targetImage, config, outputPath)` | 为图片添加水印并保存 | +| `loadImage(path)` | 从文件加载图片 | +| `buildBytes(image)` | BufferedImage转byte数组 | +| `storeBytes(buf, fullPath)` | byte数组保存为文件 | + +### 4. WatermarkPersistence.java (持久化类) +配置保存与加载功能: + +| 方法 | 说明 | +|------|------| +| `saveConfiguration(config)` | 保存配置到默认文件 | +| `saveConfiguration(config, filePath)` | 保存配置到指定文件 | +| `loadConfiguration()` | 从默认文件加载配置 | +| `loadConfiguration(filePath)` | 从指定文件加载配置 | + +## 使用示例 + +### 基本用法 +```java +// 创建默认水印画布 +BufferedImage watermark = WatermarkUtil.createWatermarkCanvas(800, 600, new WatermarkConfiguration()); + +// 保存水印图片 +WatermarkUtil.createWatermarkCanvasAndStore(800, 600, new WatermarkConfiguration(), "output/watermark.png"); +``` + +### 自定义配置 +```java +WatermarkConfiguration config = new WatermarkConfiguration() + .setText("机密文件") + .setOpacity(0.5f) + .setHorizontalSpacing(150) + .setVerticalSpacing(100) + .setFontFamily("黑体") + .setFontSize(40) + .setColor(Color.RED) + .setBold(true) + .setRotation(-45.0); + +BufferedImage watermark = WatermarkUtil.createWatermarkCanvas(800, 600, config); +``` + +### 位置模式 +```java +// 左上角水印 +WatermarkConfiguration config = new WatermarkConfiguration("LOGO") + .setPosition(WatermarkPosition.TOP_LEFT) + .setOpacity(0.8f) + .setRotation(0.0); + +// 平铺水印 +WatermarkConfiguration tiledConfig = new WatermarkConfiguration("内部使用") + .setPosition(WatermarkPosition.TILED); +``` + +### 给已有图片添加水印 +```java +BufferedImage originalImage = WatermarkUtil.loadImage("original.png"); +WatermarkConfiguration config = new WatermarkConfiguration("已审核"); +BufferedImage result = WatermarkUtil.addWatermarkToImage(originalImage, config); +WatermarkUtil.storeBytes(WatermarkUtil.buildBytes(result), "watermarked.png"); +``` + +### 配置持久化 +```java +// 保存配置 +WatermarkConfiguration config = new WatermarkConfiguration("我的水印").setOpacity(0.4f); +WatermarkPersistence.saveConfiguration(config); + +// 加载配置 +WatermarkConfiguration loadedConfig = WatermarkPersistence.loadConfiguration(); +``` + +## 功能特点 +1. **透明度控制**:支持0-1范围精确调节 +2. **密度调节**:通过水平和垂直间距控制水印分布 +3. **样式自定义**:支持字体、字号、颜色、加粗、旋转 +4. **多种位置模式**:平铺、四角、居中共6种模式 +5. **配置持久化**:本地保存用户偏好设置 +6. **兼容性**:基于Java AWT实现,跨平台兼容 + +## 测试说明 +运行单元测试类 `WatermarkTest` 可验证所有功能: +```bash +java -ea -cp out cn.localhost01.seal.WatermarkTest +``` + +测试输出目录:`test_output/` \ No newline at end of file diff --git a/src/Main.java b/src/Main.java index 53ace54..f975662 100644 --- a/src/Main.java +++ b/src/Main.java @@ -128,7 +128,7 @@ public static void main(String[] args) throws Exception { //1.生成公章 try { - SealUtil.buildAndStoreSeal(configuration, "C:\\Users\\localhost01\\Desktop\\公章.png"); + SealUtil.buildAndStoreSeal(configuration, "D:\\公章.png"); } catch (IOException e) { e.printStackTrace(); } @@ -136,7 +136,7 @@ public static void main(String[] args) throws Exception { //2.生成私章 SealFont font = new SealFont(); font.setFontSize(120).setBold(true).setFontText("诸葛孔明"); - SealUtil.buildAndStorePersonSeal(300, 16, font, "印", "C:\\Users\\localhost01\\Desktop\\私章.png"); + SealUtil.buildAndStorePersonSeal(300, 16, font, "印", "D:\\私章.png"); } } diff --git a/src/cn/localhost01/seal/WatermarkPersistence.java b/src/cn/localhost01/seal/WatermarkPersistence.java new file mode 100644 index 0000000..2c43f2e --- /dev/null +++ b/src/cn/localhost01/seal/WatermarkPersistence.java @@ -0,0 +1,111 @@ +package cn.localhost01.seal; + +import cn.localhost01.seal.configuration.WatermarkConfiguration; +import cn.localhost01.seal.configuration.WatermarkPosition; + +import java.awt.*; +import java.io.*; +import java.util.Properties; + +public class WatermarkPersistence { + private static final String CONFIG_FILE = "watermark_config.properties"; + private static final String KEY_TEXT = "watermark.text"; + private static final String KEY_OPACITY = "watermark.opacity"; + private static final String KEY_HORIZONTAL_SPACING = "watermark.horizontalSpacing"; + private static final String KEY_VERTICAL_SPACING = "watermark.verticalSpacing"; + private static final String KEY_FONT_FAMILY = "watermark.fontFamily"; + private static final String KEY_FONT_SIZE = "watermark.fontSize"; + private static final String KEY_COLOR_RED = "watermark.color.red"; + private static final String KEY_COLOR_GREEN = "watermark.color.green"; + private static final String KEY_COLOR_BLUE = "watermark.color.blue"; + private static final String KEY_IS_BOLD = "watermark.isBold"; + private static final String KEY_ROTATION = "watermark.rotation"; + private static final String KEY_POSITION = "watermark.position"; + + public static void saveConfiguration(WatermarkConfiguration config) throws IOException { + saveConfiguration(config, CONFIG_FILE); + } + + public static void saveConfiguration(WatermarkConfiguration config, String filePath) throws IOException { + Properties props = new Properties(); + props.setProperty(KEY_TEXT, config.getText()); + props.setProperty(KEY_OPACITY, String.valueOf(config.getOpacity())); + props.setProperty(KEY_HORIZONTAL_SPACING, String.valueOf(config.getHorizontalSpacing())); + props.setProperty(KEY_VERTICAL_SPACING, String.valueOf(config.getVerticalSpacing())); + props.setProperty(KEY_FONT_FAMILY, config.getFontFamily()); + props.setProperty(KEY_FONT_SIZE, String.valueOf(config.getFontSize())); + props.setProperty(KEY_COLOR_RED, String.valueOf(config.getColor().getRed())); + props.setProperty(KEY_COLOR_GREEN, String.valueOf(config.getColor().getGreen())); + props.setProperty(KEY_COLOR_BLUE, String.valueOf(config.getColor().getBlue())); + props.setProperty(KEY_IS_BOLD, String.valueOf(config.isBold())); + props.setProperty(KEY_ROTATION, String.valueOf(config.getRotation())); + props.setProperty(KEY_POSITION, config.getPosition().name()); + + try (OutputStream out = new FileOutputStream(filePath)) { + props.store(out, "Watermark Configuration"); + } + } + + public static WatermarkConfiguration loadConfiguration() throws IOException { + return loadConfiguration(CONFIG_FILE); + } + + public static WatermarkConfiguration loadConfiguration(String filePath) throws IOException { + File configFile = new File(filePath); + if (!configFile.exists()) { + return new WatermarkConfiguration(); + } + + Properties props = new Properties(); + try (InputStream in = new FileInputStream(filePath)) { + props.load(in); + } + + WatermarkConfiguration config = new WatermarkConfiguration(); + + config.setText(props.getProperty(KEY_TEXT, "水印")); + + try { + float opacity = Float.parseFloat(props.getProperty(KEY_OPACITY, "0.3")); + config.setOpacity(opacity); + } catch (NumberFormatException ignored) {} + + try { + int hSpacing = Integer.parseInt(props.getProperty(KEY_HORIZONTAL_SPACING, "200")); + config.setHorizontalSpacing(hSpacing); + } catch (NumberFormatException ignored) {} + + try { + int vSpacing = Integer.parseInt(props.getProperty(KEY_VERTICAL_SPACING, "150")); + config.setVerticalSpacing(vSpacing); + } catch (NumberFormatException ignored) {} + + config.setFontFamily(props.getProperty(KEY_FONT_FAMILY, "宋体")); + + try { + int fontSize = Integer.parseInt(props.getProperty(KEY_FONT_SIZE, "30")); + config.setFontSize(fontSize); + } catch (NumberFormatException ignored) {} + + try { + int r = Integer.parseInt(props.getProperty(KEY_COLOR_RED, "128")); + int g = Integer.parseInt(props.getProperty(KEY_COLOR_GREEN, "128")); + int b = Integer.parseInt(props.getProperty(KEY_COLOR_BLUE, "128")); + config.setColor(new Color(r, g, b)); + } catch (NumberFormatException ignored) {} + + config.setBold(Boolean.parseBoolean(props.getProperty(KEY_IS_BOLD, "false"))); + + try { + double rotation = Double.parseDouble(props.getProperty(KEY_ROTATION, "-30.0")); + config.setRotation(rotation); + } catch (NumberFormatException ignored) {} + + try { + String positionStr = props.getProperty(KEY_POSITION, "TILED"); + config.setPosition(WatermarkPosition.valueOf(positionStr)); + } catch (IllegalArgumentException ignored) {} + + return config; + } +} diff --git a/src/cn/localhost01/seal/WatermarkTest.java b/src/cn/localhost01/seal/WatermarkTest.java new file mode 100644 index 0000000..9d7f75d --- /dev/null +++ b/src/cn/localhost01/seal/WatermarkTest.java @@ -0,0 +1,318 @@ +package cn.localhost01.seal; + +import cn.localhost01.seal.configuration.WatermarkConfiguration; +import cn.localhost01.seal.configuration.WatermarkPosition; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +public class WatermarkTest { + + private static final String TEST_OUTPUT_DIR = "test_output/"; + + private static void assertFileExists(String filePath) { + File file = new File(filePath); + assert file.exists() : "文件不存在: " + filePath; + assert file.length() > 0 : "文件为空: " + filePath; + } + + private static void assertImageDimensions(BufferedImage image, int expectedWidth, int expectedHeight) { + assert image != null : "图像不能为空"; + assert image.getWidth() == expectedWidth : "宽度不匹配: 期望 " + expectedWidth + ", 实际 " + image.getWidth(); + assert image.getHeight() == expectedHeight : "高度不匹配: 期望 " + expectedHeight + ", 实际 " + image.getHeight(); + } + + private static void assertImageHasContent(BufferedImage image) throws IOException { + boolean hasNonTransparentPixel = false; + int width = image.getWidth(); + int height = image.getHeight(); + int step = Math.max(1, Math.min(width, height) / 50); + for (int x = 0; x < width; x += step) { + for (int y = 0; y < height; y += step) { + int pixel = image.getRGB(x, y); + if ((pixel >> 24) != 0x00) { + hasNonTransparentPixel = true; + break; + } + } + if (hasNonTransparentPixel) break; + } + assert hasNonTransparentPixel : "图像没有可见内容"; + } + + private static void assertWatermarkApplied(BufferedImage original, BufferedImage watermarked) { + assert watermarked != null : "水印图像不能为空"; + assert original.getWidth() == watermarked.getWidth() : "水印后宽度改变"; + assert original.getHeight() == watermarked.getHeight() : "水印后高度改变"; + boolean hasDifference = false; + int checkCount = 0; + for (int x = 0; x < original.getWidth() && checkCount < 1000; x += 10) { + for (int y = 0; y < original.getHeight() && checkCount < 1000; y += 10) { + if (original.getRGB(x, y) != watermarked.getRGB(x, y)) { + hasDifference = true; + break; + } + checkCount++; + } + if (hasDifference) break; + } + assert hasDifference : "水印未应用到图像"; + } + + public static void main(String[] args) throws Exception { + System.out.println("=== 水印功能单元测试开始 ==="); + + File testDir = new File(TEST_OUTPUT_DIR); + if (!testDir.exists()) { + testDir.mkdirs(); + } + + testDefaultWatermark(); + testOpacitySettings(); + testDensitySettings(); + testFontSettings(); + testPositionSettings(); + testPersistence(); + testAddWatermarkToExistingImage(); + + System.out.println("=== 水印功能单元测试完成 ==="); + System.out.println("测试输出目录: " + new File(TEST_OUTPUT_DIR).getAbsolutePath()); + } + + private static void testDefaultWatermark() throws IOException { + System.out.print("测试默认水印配置... "); + WatermarkConfiguration config = new WatermarkConfiguration(); + + assert "水印".equals(config.getText()) : "默认文本不正确"; + assert config.getOpacity() == 0.3f : "默认透明度不正确"; + assert config.getHorizontalSpacing() == 200 : "默认水平间距不正确"; + assert config.getPosition() == WatermarkPosition.TILED : "默认位置不正确"; + + BufferedImage result = WatermarkUtil.createWatermarkCanvas(800, 600, config); + assertImageDimensions(result, 800, 600); + assertImageHasContent(result); + + String filePath = TEST_OUTPUT_DIR + "1_default_watermark.png"; + WatermarkUtil.storeBytes(WatermarkUtil.buildBytes(result), filePath); + assertFileExists(filePath); + + System.out.println("通过"); + } + + private static void testOpacitySettings() throws IOException { + System.out.print("测试透明度设置... "); + + WatermarkConfiguration config1 = new WatermarkConfiguration("透明度测试") + .setOpacity(0.1f); + assert config1.getOpacity() == 0.1f : "透明度0.1设置失败"; + BufferedImage result1 = WatermarkUtil.createWatermarkCanvas(400, 300, config1); + assertImageDimensions(result1, 400, 300); + String file1 = TEST_OUTPUT_DIR + "2_opacity_0.1.png"; + WatermarkUtil.storeBytes(WatermarkUtil.buildBytes(result1), file1); + assertFileExists(file1); + + WatermarkConfiguration config2 = new WatermarkConfiguration("透明度测试") + .setOpacity(0.5f); + assert config2.getOpacity() == 0.5f : "透明度0.5设置失败"; + BufferedImage result2 = WatermarkUtil.createWatermarkCanvas(400, 300, config2); + assertImageDimensions(result2, 400, 300); + String file2 = TEST_OUTPUT_DIR + "2_opacity_0.5.png"; + WatermarkUtil.storeBytes(WatermarkUtil.buildBytes(result2), file2); + assertFileExists(file2); + + WatermarkConfiguration config3 = new WatermarkConfiguration("透明度测试") + .setOpacity(1.0f); + assert config3.getOpacity() == 1.0f : "透明度1.0设置失败"; + BufferedImage result3 = WatermarkUtil.createWatermarkCanvas(400, 300, config3); + assertImageDimensions(result3, 400, 300); + String file3 = TEST_OUTPUT_DIR + "2_opacity_1.0.png"; + WatermarkUtil.storeBytes(WatermarkUtil.buildBytes(result3), file3); + assertFileExists(file3); + + WatermarkConfiguration configInvalidLow = new WatermarkConfiguration().setOpacity(-0.5f); + assert configInvalidLow.getOpacity() == 0.0f : "透明度边界最小值验证失败"; + + WatermarkConfiguration configInvalidHigh = new WatermarkConfiguration().setOpacity(2.0f); + assert configInvalidHigh.getOpacity() == 1.0f : "透明度边界最大值验证失败"; + + System.out.println("通过"); + } + + private static void testDensitySettings() throws IOException { + System.out.print("测试密度(间距)设置... "); + + WatermarkConfiguration config1 = new WatermarkConfiguration("高密度") + .setHorizontalSpacing(100) + .setVerticalSpacing(80); + assert config1.getHorizontalSpacing() == 100 : "水平间距100设置失败"; + assert config1.getVerticalSpacing() == 80 : "垂直间距80设置失败"; + BufferedImage result1 = WatermarkUtil.createWatermarkCanvas(800, 600, config1); + assertImageDimensions(result1, 800, 600); + assertImageHasContent(result1); + String file1 = TEST_OUTPUT_DIR + "3_high_density.png"; + WatermarkUtil.storeBytes(WatermarkUtil.buildBytes(result1), file1); + assertFileExists(file1); + + WatermarkConfiguration config2 = new WatermarkConfiguration("低密度") + .setHorizontalSpacing(300) + .setVerticalSpacing(200); + assert config2.getHorizontalSpacing() == 300 : "水平间距300设置失败"; + assert config2.getVerticalSpacing() == 200 : "垂直间距200设置失败"; + BufferedImage result2 = WatermarkUtil.createWatermarkCanvas(800, 600, config2); + assertImageDimensions(result2, 800, 600); + assertImageHasContent(result2); + String file2 = TEST_OUTPUT_DIR + "3_low_density.png"; + WatermarkUtil.storeBytes(WatermarkUtil.buildBytes(result2), file2); + assertFileExists(file2); + + WatermarkConfiguration configInvalid = new WatermarkConfiguration() + .setHorizontalSpacing(-10) + .setVerticalSpacing(0); + assert configInvalid.getHorizontalSpacing() == 1 : "水平间距边界验证失败"; + assert configInvalid.getVerticalSpacing() == 1 : "垂直间距边界验证失败"; + + System.out.println("通过"); + } + + private static void testFontSettings() throws IOException { + System.out.print("测试字体样式设置... "); + + WatermarkConfiguration config1 = new WatermarkConfiguration("楷体水印") + .setFontFamily("楷体") + .setFontSize(40) + .setColor(Color.BLUE) + .setBold(true); + assert "楷体".equals(config1.getFontFamily()) : "字体设置失败"; + assert config1.getFontSize() == 40 : "字号40设置失败"; + assert Color.BLUE.equals(config1.getColor()) : "颜色设置失败"; + assert config1.isBold() == true : "加粗设置失败"; + BufferedImage result1 = WatermarkUtil.createWatermarkCanvas(800, 600, config1); + assertImageDimensions(result1, 800, 600); + assertImageHasContent(result1); + String file1 = TEST_OUTPUT_DIR + "4_font_kaiti_blue_bold.png"; + WatermarkUtil.storeBytes(WatermarkUtil.buildBytes(result1), file1); + assertFileExists(file1); + + WatermarkConfiguration config2 = new WatermarkConfiguration("黑体红字") + .setFontFamily("黑体") + .setFontSize(25) + .setColor(Color.RED); + assert "黑体".equals(config2.getFontFamily()) : "字体设置失败"; + assert config2.getFontSize() == 25 : "字号25设置失败"; + assert Color.RED.equals(config2.getColor()) : "颜色设置失败"; + BufferedImage result2 = WatermarkUtil.createWatermarkCanvas(800, 600, config2); + assertImageDimensions(result2, 800, 600); + assertImageHasContent(result2); + String file2 = TEST_OUTPUT_DIR + "4_font_heiti_red.png"; + WatermarkUtil.storeBytes(WatermarkUtil.buildBytes(result2), file2); + assertFileExists(file2); + + WatermarkConfiguration configInvalidSize = new WatermarkConfiguration().setFontSize(-5); + assert configInvalidSize.getFontSize() == 1 : "字号边界验证失败"; + + System.out.println("通过"); + } + + private static void testPositionSettings() throws IOException { + System.out.print("测试位置设置... "); + + assert WatermarkPosition.values().length == 6 : "位置枚举数量不正确"; + + for (WatermarkPosition position : WatermarkPosition.values()) { + WatermarkConfiguration config = new WatermarkConfiguration(position.name()) + .setPosition(position) + .setOpacity(0.8f) + .setFontSize(30) + .setRotation(0.0); + assert config.getPosition() == position : "位置设置失败: " + position; + BufferedImage result = WatermarkUtil.createWatermarkCanvas(400, 300, config); + assertImageDimensions(result, 400, 300); + assertImageHasContent(result); + String filePath = TEST_OUTPUT_DIR + "5_position_" + position.name().toLowerCase() + ".png"; + WatermarkUtil.storeBytes(WatermarkUtil.buildBytes(result), filePath); + assertFileExists(filePath); + } + + System.out.println("通过"); + } + + private static void testPersistence() throws IOException { + System.out.print("测试持久化功能... "); + + WatermarkConfiguration original = new WatermarkConfiguration("持久化测试") + .setOpacity(0.4f) + .setHorizontalSpacing(150) + .setVerticalSpacing(120) + .setFontFamily("黑体") + .setFontSize(35) + .setColor(Color.MAGENTA) + .setBold(true) + .setRotation(-15.0) + .setPosition(WatermarkPosition.CENTER); + + String testConfigPath = TEST_OUTPUT_DIR + "test_watermark_config.properties"; + WatermarkPersistence.saveConfiguration(original, testConfigPath); + + WatermarkConfiguration loaded = WatermarkPersistence.loadConfiguration(testConfigPath); + + assert original.getText().equals(loaded.getText()) : "文本不匹配"; + assert original.getOpacity() == loaded.getOpacity() : "透明度不匹配"; + assert original.getHorizontalSpacing() == loaded.getHorizontalSpacing() : "水平间距不匹配"; + assert original.getVerticalSpacing() == loaded.getVerticalSpacing() : "垂直间距不匹配"; + assert original.getFontFamily().equals(loaded.getFontFamily()) : "字体不匹配"; + assert original.getFontSize() == loaded.getFontSize() : "字号不匹配"; + assert original.getColor().equals(loaded.getColor()) : "颜色不匹配"; + assert original.isBold().equals(loaded.isBold()) : "加粗不匹配"; + assert original.getRotation().equals(loaded.getRotation()) : "旋转不匹配"; + assert original.getPosition() == loaded.getPosition() : "位置不匹配"; + + System.out.println("通过"); + } + + private static void testAddWatermarkToExistingImage() throws Exception { + System.out.print("测试给已有图片添加水印... "); + + BufferedImage testOriginal = new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB); + Graphics2D g = testOriginal.createGraphics(); + g.setColor(Color.WHITE); + g.fillRect(0, 0, 200, 200); + g.dispose(); + + WatermarkConfiguration config = new WatermarkConfiguration("测试水印") + .setOpacity(0.5f) + .setColor(Color.BLACK); + BufferedImage result = WatermarkUtil.addWatermarkToImage(testOriginal, config); + assertImageDimensions(result, 200, 200); + assertWatermarkApplied(testOriginal, result); + + BufferedImage nullResult1 = WatermarkUtil.addWatermarkToImage(null, config); + assert nullResult1 == null : "空图像应返回null"; + + BufferedImage nullResult2 = WatermarkUtil.addWatermarkToImage(testOriginal, null); + assert nullResult2 == testOriginal : "空配置应返回原图"; + + String[] sampleImages = { + "img/公章0.png", + "img/私章0.png" + }; + + for (String imgPath : sampleImages) { + File imgFile = new File(imgPath); + if (imgFile.exists()) { + BufferedImage originalImage = WatermarkUtil.loadImage(imgPath); + assert originalImage != null : "无法加载测试图像: " + imgPath; + BufferedImage watermarked = WatermarkUtil.addWatermarkToImage(originalImage, config); + assert watermarked != null : "水印添加失败"; + assertImageDimensions(watermarked, originalImage.getWidth(), originalImage.getHeight()); + String fileName = new File(imgPath).getName(); + String outputPath = TEST_OUTPUT_DIR + "6_watermarked_" + fileName; + WatermarkUtil.storeBytes(WatermarkUtil.buildBytes(watermarked), outputPath); + assertFileExists(outputPath); + } + } + + System.out.println("通过"); + } +} diff --git a/src/cn/localhost01/seal/WatermarkUtil.java b/src/cn/localhost01/seal/WatermarkUtil.java new file mode 100644 index 0000000..5ccffac --- /dev/null +++ b/src/cn/localhost01/seal/WatermarkUtil.java @@ -0,0 +1,176 @@ +package cn.localhost01.seal; + +import cn.localhost01.seal.configuration.WatermarkConfiguration; +import cn.localhost01.seal.configuration.WatermarkPosition; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.*; + +public abstract class WatermarkUtil { + + public static byte[] buildBytes(BufferedImage image) throws IOException { + try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) { + ImageIO.write(image, "png", outStream); + return outStream.toByteArray(); + } + } + + public static void storeBytes(byte[] buf, String fullPath) throws IOException { + File file = new File(fullPath); + File dir = file.getParentFile(); + if (dir != null && !dir.exists()) { + dir.mkdirs(); + } + try (FileOutputStream fos = new FileOutputStream(file); + BufferedOutputStream bos = new BufferedOutputStream(fos)) { + bos.write(buf); + } + } + + public static BufferedImage addWatermarkToImage(BufferedImage targetImage, WatermarkConfiguration config) { + if (targetImage == null || config == null) { + return targetImage; + } + + int width = targetImage.getWidth(); + int height = targetImage.getHeight(); + + BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = result.createGraphics(); + + g2d.drawImage(targetImage, 0, 0, null); + + applyWatermark(g2d, width, height, config); + + g2d.dispose(); + return result; + } + + public static void addWatermarkToImageAndStore(BufferedImage targetImage, WatermarkConfiguration config, String outputPath) throws IOException { + BufferedImage result = addWatermarkToImage(targetImage, config); + storeBytes(buildBytes(result), outputPath); + } + + public static BufferedImage createWatermarkCanvas(int width, int height, WatermarkConfiguration config) { + if (config == null) { + config = new WatermarkConfiguration(); + } + + BufferedImage canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = canvas.createGraphics(); + + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 0)); + g2d.fillRect(0, 0, width, height); + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 1)); + + applyWatermark(g2d, width, height, config); + + g2d.dispose(); + return canvas; + } + + public static void createWatermarkCanvasAndStore(int width, int height, WatermarkConfiguration config, String outputPath) throws IOException { + BufferedImage result = createWatermarkCanvas(width, height, config); + storeBytes(buildBytes(result), outputPath); + } + + private static void applyWatermark(Graphics2D g2d, int width, int height, WatermarkConfiguration config) { + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + AlphaComposite alphaComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, config.getOpacity()); + g2d.setComposite(alphaComposite); + + int fontStyle = config.isBold() ? Font.BOLD : Font.PLAIN; + Font font = new Font(config.getFontFamily(), fontStyle, config.getFontSize()); + g2d.setFont(font); + g2d.setColor(config.getColor()); + + FontMetrics metrics = g2d.getFontMetrics(); + Rectangle2D textBounds = metrics.getStringBounds(config.getText(), g2d); + int textWidth = (int) textBounds.getWidth(); + int textHeight = (int) textBounds.getHeight(); + + if (config.getPosition() == WatermarkPosition.TILED) { + drawTiledWatermark(g2d, width, height, config, textWidth, textHeight); + } else { + drawSinglePositionWatermark(g2d, width, height, config, textWidth, textHeight); + } + } + + private static void drawTiledWatermark(Graphics2D g2d, int width, int height, + WatermarkConfiguration config, int textWidth, int textHeight) { + int hSpacing = config.getHorizontalSpacing(); + int vSpacing = config.getVerticalSpacing(); + double rotation = Math.toRadians(config.getRotation()); + + int effectiveWidth = textWidth + hSpacing; + int effectiveHeight = textHeight + vSpacing; + + int cols = (width / effectiveWidth) + 2; + int rows = (height / effectiveHeight) + 2; + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + int x = col * effectiveWidth; + int y = row * effectiveHeight + textHeight; + + if (rotation != 0) { + g2d.rotate(rotation, x + textWidth / 2.0, y - textHeight / 2.0); + } + + g2d.drawString(config.getText(), x, y); + + if (rotation != 0) { + g2d.rotate(-rotation, x + textWidth / 2.0, y - textHeight / 2.0); + } + } + } + } + + private static void drawSinglePositionWatermark(Graphics2D g2d, int width, int height, + WatermarkConfiguration config, int textWidth, int textHeight) { + int x = 0, y = 0; + double rotation = Math.toRadians(config.getRotation()); + + switch (config.getPosition()) { + case TOP_LEFT: + x = 10; + y = textHeight + 10; + break; + case TOP_RIGHT: + x = width - textWidth - 10; + y = textHeight + 10; + break; + case BOTTOM_LEFT: + x = 10; + y = height - 10; + break; + case BOTTOM_RIGHT: + x = width - textWidth - 10; + y = height - 10; + break; + case CENTER: + x = (width - textWidth) / 2; + y = (height + textHeight) / 2; + break; + } + + if (rotation != 0) { + g2d.rotate(rotation, x + textWidth / 2.0, y - textHeight / 2.0); + } + + g2d.drawString(config.getText(), x, y); + + if (rotation != 0) { + g2d.rotate(-rotation, x + textWidth / 2.0, y - textHeight / 2.0); + } + } + + public static BufferedImage loadImage(String path) throws IOException { + return ImageIO.read(new File(path)); + } +} diff --git a/src/cn/localhost01/seal/configuration/WatermarkConfiguration.java b/src/cn/localhost01/seal/configuration/WatermarkConfiguration.java new file mode 100644 index 0000000..2823cf5 --- /dev/null +++ b/src/cn/localhost01/seal/configuration/WatermarkConfiguration.java @@ -0,0 +1,115 @@ +package cn.localhost01.seal.configuration; + +import java.awt.*; + +public class WatermarkConfiguration { + private String text = "水印"; + private float opacity = 0.3f; + private int horizontalSpacing = 200; + private int verticalSpacing = 150; + private String fontFamily = "宋体"; + private int fontSize = 30; + private Color color = Color.GRAY; + private Boolean isBold = false; + private Double rotation = -30.0; + private WatermarkPosition position = WatermarkPosition.TILED; + + public WatermarkConfiguration() { + } + + public WatermarkConfiguration(String text) { + this.text = text; + } + + public WatermarkConfiguration setText(String text) { + this.text = text; + return this; + } + + public WatermarkConfiguration setOpacity(float opacity) { + if (opacity < 0) opacity = 0; + if (opacity > 1) opacity = 1; + this.opacity = opacity; + return this; + } + + public WatermarkConfiguration setHorizontalSpacing(int horizontalSpacing) { + this.horizontalSpacing = Math.max(1, horizontalSpacing); + return this; + } + + public WatermarkConfiguration setVerticalSpacing(int verticalSpacing) { + this.verticalSpacing = Math.max(1, verticalSpacing); + return this; + } + + public WatermarkConfiguration setFontFamily(String fontFamily) { + this.fontFamily = fontFamily; + return this; + } + + public WatermarkConfiguration setFontSize(int fontSize) { + this.fontSize = Math.max(1, fontSize); + return this; + } + + public WatermarkConfiguration setColor(Color color) { + this.color = color; + return this; + } + + public WatermarkConfiguration setBold(Boolean bold) { + isBold = bold; + return this; + } + + public WatermarkConfiguration setRotation(Double rotation) { + this.rotation = rotation; + return this; + } + + public WatermarkConfiguration setPosition(WatermarkPosition position) { + this.position = position; + return this; + } + + public String getText() { + return text; + } + + public float getOpacity() { + return opacity; + } + + public int getHorizontalSpacing() { + return horizontalSpacing; + } + + public int getVerticalSpacing() { + return verticalSpacing; + } + + public String getFontFamily() { + return fontFamily; + } + + public int getFontSize() { + return fontSize; + } + + public Color getColor() { + return color; + } + + public Boolean isBold() { + return isBold; + } + + public Double getRotation() { + return rotation; + } + + public WatermarkPosition getPosition() { + return position; + } +} diff --git a/src/cn/localhost01/seal/configuration/WatermarkPosition.java b/src/cn/localhost01/seal/configuration/WatermarkPosition.java new file mode 100644 index 0000000..8f36037 --- /dev/null +++ b/src/cn/localhost01/seal/configuration/WatermarkPosition.java @@ -0,0 +1,10 @@ +package cn.localhost01.seal.configuration; + +public enum WatermarkPosition { + TILED, + TOP_LEFT, + TOP_RIGHT, + BOTTOM_LEFT, + BOTTOM_RIGHT, + CENTER +} diff --git a/test_output/1_default_watermark.png b/test_output/1_default_watermark.png new file mode 100644 index 0000000..d9cce65 Binary files /dev/null and b/test_output/1_default_watermark.png differ diff --git a/test_output/2_opacity_0.1.png b/test_output/2_opacity_0.1.png new file mode 100644 index 0000000..038428f Binary files /dev/null and b/test_output/2_opacity_0.1.png differ diff --git a/test_output/2_opacity_0.5.png b/test_output/2_opacity_0.5.png new file mode 100644 index 0000000..9abc3bd Binary files /dev/null and b/test_output/2_opacity_0.5.png differ diff --git a/test_output/2_opacity_1.0.png b/test_output/2_opacity_1.0.png new file mode 100644 index 0000000..018b8c1 Binary files /dev/null and b/test_output/2_opacity_1.0.png differ diff --git a/test_output/3_high_density.png b/test_output/3_high_density.png new file mode 100644 index 0000000..cb3cc7d Binary files /dev/null and b/test_output/3_high_density.png differ diff --git a/test_output/3_low_density.png b/test_output/3_low_density.png new file mode 100644 index 0000000..bb0abe1 Binary files /dev/null and b/test_output/3_low_density.png differ diff --git a/test_output/4_font_heiti_red.png b/test_output/4_font_heiti_red.png new file mode 100644 index 0000000..d76f4ac Binary files /dev/null and b/test_output/4_font_heiti_red.png differ diff --git a/test_output/4_font_kaiti_blue_bold.png b/test_output/4_font_kaiti_blue_bold.png new file mode 100644 index 0000000..767fbc8 Binary files /dev/null and b/test_output/4_font_kaiti_blue_bold.png differ diff --git a/test_output/5_position_bottom_left.png b/test_output/5_position_bottom_left.png new file mode 100644 index 0000000..7eaf006 Binary files /dev/null and b/test_output/5_position_bottom_left.png differ diff --git a/test_output/5_position_bottom_right.png b/test_output/5_position_bottom_right.png new file mode 100644 index 0000000..abe1bdb Binary files /dev/null and b/test_output/5_position_bottom_right.png differ diff --git a/test_output/5_position_center.png b/test_output/5_position_center.png new file mode 100644 index 0000000..e8cba17 Binary files /dev/null and b/test_output/5_position_center.png differ diff --git a/test_output/5_position_tiled.png b/test_output/5_position_tiled.png new file mode 100644 index 0000000..cefc70a Binary files /dev/null and b/test_output/5_position_tiled.png differ diff --git a/test_output/5_position_top_left.png b/test_output/5_position_top_left.png new file mode 100644 index 0000000..e441a59 Binary files /dev/null and b/test_output/5_position_top_left.png differ diff --git a/test_output/5_position_top_right.png b/test_output/5_position_top_right.png new file mode 100644 index 0000000..59e41e4 Binary files /dev/null and b/test_output/5_position_top_right.png differ diff --git "a/test_output/6_watermarked_\345\205\254\347\253\2400.png" "b/test_output/6_watermarked_\345\205\254\347\253\2400.png" new file mode 100644 index 0000000..141f789 Binary files /dev/null and "b/test_output/6_watermarked_\345\205\254\347\253\2400.png" differ diff --git "a/test_output/6_watermarked_\347\247\201\347\253\2400.png" "b/test_output/6_watermarked_\347\247\201\347\253\2400.png" new file mode 100644 index 0000000..e717069 Binary files /dev/null and "b/test_output/6_watermarked_\347\247\201\347\253\2400.png" differ diff --git a/test_output/test_watermark_config.properties b/test_output/test_watermark_config.properties new file mode 100644 index 0000000..6686621 --- /dev/null +++ b/test_output/test_watermark_config.properties @@ -0,0 +1,14 @@ +#Watermark Configuration +#Wed Mar 11 17:55:39 CST 2026 +watermark.color.blue=255 +watermark.opacity=0.4 +watermark.rotation=-15.0 +watermark.text=\u6301\u4E45\u5316\u6D4B\u8BD5 +watermark.verticalSpacing=120 +watermark.fontSize=35 +watermark.position=CENTER +watermark.isBold=true +watermark.color.red=255 +watermark.color.green=0 +watermark.fontFamily=\u9ED1\u4F53 +watermark.horizontalSpacing=150